diff --git a/Cargo.lock b/Cargo.lock index a17184eea1..433c8b4a2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -837,7 +837,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.2.0" -source = "git+https://github.com/jntrnr/reedline?branch=main#88bded3417e7f6c1242b444f403448de583357f0" +source = "git+https://github.com/nushell/reedline?branch=main#88bded3417e7f6c1242b444f403448de583357f0" dependencies = [ "chrono", "crossterm", diff --git a/Cargo.toml b/Cargo.toml index 5f43cb1172..3d819c0621 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ edition = "2018" members = ["crates/nu-cli", "crates/nu-engine", "crates/nu-parser", "crates/nu-command", "crates/nu-protocol"] [dependencies] -reedline = { git = "https://github.com/jntrnr/reedline", branch = "main" } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } crossterm = "0.21.*" nu-cli = { path="./crates/nu-cli" } nu-command = { path="./crates/nu-command" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..25623d08e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Nushell Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/TODO.md b/TODO.md index 0a051b75b9..13df448caa 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,7 @@ - [x] Externals - [x] Modules and imports - [x] Exports +- [x] Source - [ ] Input/output types - [ ] Support for `$in` - [ ] Value serialization @@ -32,7 +33,6 @@ - [ ] ctrl-c support - [ ] operator overflow - [ ] finish operator type-checking -- [ ] Source - [ ] Overlays (replacement for `autoenv`) ## Maybe: diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 5b47189196..3f2f90c7cc 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -12,4 +12,4 @@ nu-protocol = { path = "../nu-protocol" } miette = { version = "3.0.0", features = ["fancy"] } thiserror = "1.0.29" nu-ansi-term = "0.36.0" -reedline = { git = "https://github.com/jntrnr/reedline", branch = "main" } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 10e997054d..161801e261 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -34,7 +34,6 @@ impl Highlighter for NuHighlighter { .to_string(); output.push((Style::new(), gap)); } - let next_token = line [(shape.0.start - global_span_offset)..(shape.0.end - global_span_offset)] .to_string(); diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs index 3472f4d8b3..7418026a56 100644 --- a/crates/nu-command/src/core_commands/mod.rs +++ b/crates/nu-command/src/core_commands/mod.rs @@ -7,6 +7,7 @@ mod hide; mod if_; mod let_; mod module; +mod source; mod use_; pub use alias::Alias; @@ -18,4 +19,5 @@ pub use hide::Hide; pub use if_::If; pub use let_::Let; pub use module::Module; +pub use source::Source; pub use use_::Use; diff --git a/crates/nu-command/src/core_commands/source.rs b/crates/nu-command/src/core_commands/source.rs new file mode 100644 index 0000000000..aacd3564cd --- /dev/null +++ b/crates/nu-command/src/core_commands/source.rs @@ -0,0 +1,43 @@ +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; + +/// Source a file for environment variables. +pub struct Source; + +impl Command for Source { + fn name(&self) -> &str { + "source" + } + + fn signature(&self) -> Signature { + Signature::build("source").required( + "filename", + SyntaxShape::Filepath, + "the filepath to the script file to source", + ) + } + + fn usage(&self) -> &str { + "Runs a script file in the current context." + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + input: Value, + ) -> Result { + // Note: this hidden positional is the block_id that corresponded to the 0th position + // it is put here by the parser + let block_id: i64 = call.req(context, 1)?; + + let block = context + .engine_state + .borrow() + .get_block(block_id as usize) + .clone(); + eval_block(context, &block, input) + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 30065069dd..989cee484a 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -17,6 +17,7 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Benchmark)); working_set.add_decl(Box::new(BuildString)); working_set.add_decl(Box::new(Cd)); + working_set.add_decl(Box::new(Cp)); working_set.add_decl(Box::new(Def)); working_set.add_decl(Box::new(Do)); working_set.add_decl(Box::new(Each)); @@ -50,6 +51,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Git)); working_set.add_decl(Box::new(GitCheckout)); + working_set.add_decl(Box::new(Source)); + let sig = Signature::build("exit"); working_set.add_decl(sig.predeclare()); let sig = Signature::build("vars"); diff --git a/crates/nu-command/src/filesystem/cp.rs b/crates/nu-command/src/filesystem/cp.rs new file mode 100644 index 0000000000..79751f2c93 --- /dev/null +++ b/crates/nu-command/src/filesystem/cp.rs @@ -0,0 +1,169 @@ +use std::env::current_dir; +use std::path::PathBuf; + +use nu_engine::CallExt; +use nu_path::canonicalize_with; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EvaluationContext}; +use nu_protocol::{ShellError, Signature, SyntaxShape, Value}; + +use crate::filesystem::util::FileStructure; + +pub struct Cp; + +impl Command for Cp { + fn name(&self) -> &str { + "cp" + } + + fn usage(&self) -> &str { + "Copy files." + } + + fn signature(&self) -> Signature { + Signature::build("cp") + .required("source", SyntaxShape::GlobPattern, "the place to copy from") + .required("destination", SyntaxShape::Filepath, "the place to copy to") + .switch( + "recursive", + "copy recursively through subdirectories", + Some('r'), + ) + } + + fn run( + &self, + context: &EvaluationContext, + call: &Call, + _input: Value, + ) -> Result { + let source: String = call.req(context, 0)?; + let destination: String = call.req(context, 1)?; + + let path: PathBuf = current_dir().unwrap(); + let source = path.join(source.as_str()); + let destination = path.join(destination.as_str()); + + let sources = + glob::glob(&source.to_string_lossy()).map_or_else(|_| Vec::new(), Iterator::collect); + if sources.is_empty() { + return Err(ShellError::FileNotFound(call.positional[0].span)); + } + + if sources.len() > 1 && !destination.is_dir() { + return Err(ShellError::MoveNotPossible { + source_message: "Can't move many files".to_string(), + source_span: call.positional[0].span, + destination_message: "into single file".to_string(), + destination_span: call.positional[1].span, + }); + } + + 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"); + if any_source_is_dir && !recursive { + return Err(ShellError::MoveNotPossibleSingle( + "Directories must be copied using \"--recursive\"".to_string(), + call.positional[0].span, + )); + } + + for entry in sources.into_iter().flatten() { + let mut sources = FileStructure::new(); + sources.walk_decorate(&entry)?; + + if entry.is_file() { + let sources = sources.paths_applying_with(|(source_file, _depth_level)| { + if destination.is_dir() { + let mut dest = canonicalize_with(&destination, &path)?; + if let Some(name) = entry.file_name() { + dest.push(name); + } + Ok((source_file, dest)) + } else { + Ok((source_file, destination.clone())) + } + })?; + + for (src, dst) in sources { + if src.is_file() { + std::fs::copy(&src, dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to move containing file \"{}\": {}", + src.to_string_lossy(), + e + ), + call.positional[0].span, + ) + })?; + } + } + } else if entry.is_dir() { + let destination = if !destination.exists() { + destination.clone() + } else { + match entry.file_name() { + Some(name) => destination.join(name), + None => { + return Err(ShellError::FileNotFoundCustom( + format!("containing \"{:?}\" is not a valid path", entry), + call.positional[0].span, + )) + } + } + }; + + std::fs::create_dir_all(&destination).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!("failed to recursively fill destination: {}", e), + call.positional[1].span, + ) + })?; + + let sources = sources.paths_applying_with(|(source_file, depth_level)| { + let mut dest = destination.clone(); + let path = canonicalize_with(&source_file, &path)?; + let components = path + .components() + .map(|fragment| fragment.as_os_str()) + .rev() + .take(1 + depth_level); + + components.for_each(|fragment| dest.push(fragment)); + Ok((PathBuf::from(&source_file), dest)) + })?; + + for (src, dst) in sources { + if src.is_dir() && !dst.exists() { + std::fs::create_dir_all(&dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to create containing directory \"{}\": {}", + dst.to_string_lossy(), + e + ), + call.positional[1].span, + ) + })?; + } + + if src.is_file() { + std::fs::copy(&src, &dst).map_err(|e| { + ShellError::MoveNotPossibleSingle( + format!( + "failed to move containing file \"{}\": {}", + src.to_string_lossy(), + e + ), + call.positional[0].span, + ) + })?; + } + } + } + } + + Ok(Value::Nothing { span: call.head }) + } +} diff --git a/crates/nu-command/src/filesystem/mod.rs b/crates/nu-command/src/filesystem/mod.rs index 90b697fcd8..2d2212766a 100644 --- a/crates/nu-command/src/filesystem/mod.rs +++ b/crates/nu-command/src/filesystem/mod.rs @@ -1,7 +1,10 @@ mod cd; +mod cp; mod ls; mod mv; +mod util; pub use cd::Cd; +pub use cp::Cp; pub use ls::Ls; pub use mv::Mv; diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs new file mode 100644 index 0000000000..a2d217cf05 --- /dev/null +++ b/crates/nu-command/src/filesystem/util.rs @@ -0,0 +1,81 @@ +use std::path::{Path, PathBuf}; + +use nu_path::canonicalize_with; +use nu_protocol::ShellError; + +#[derive(Default)] +pub struct FileStructure { + pub resources: Vec, +} + +#[allow(dead_code)] +impl FileStructure { + pub fn new() -> FileStructure { + FileStructure { resources: vec![] } + } + + pub fn contains_more_than_one_file(&self) -> bool { + self.resources.len() > 1 + } + + pub fn contains_files(&self) -> bool { + !self.resources.is_empty() + } + + pub fn paths_applying_with( + &mut self, + to: F, + ) -> Result, Box> + where + F: Fn((PathBuf, usize)) -> Result<(PathBuf, PathBuf), Box>, + { + self.resources + .iter() + .map(|f| (PathBuf::from(&f.location), f.at)) + .map(to) + .collect() + } + + pub fn walk_decorate(&mut self, start_path: &Path) -> Result<(), ShellError> { + self.resources = Vec::::new(); + self.build(start_path, 0)?; + self.resources.sort(); + + Ok(()) + } + + fn build(&mut self, src: &Path, lvl: usize) -> Result<(), ShellError> { + let source = canonicalize_with(src, std::env::current_dir()?)?; + + if source.is_dir() { + for entry in std::fs::read_dir(src)? { + let entry = entry?; + let path = entry.path(); + + if path.is_dir() { + self.build(&path, lvl + 1)?; + } + + self.resources.push(Resource { + location: path.to_path_buf(), + at: lvl, + }); + } + } else { + self.resources.push(Resource { + location: source, + at: lvl, + }); + } + + Ok(()) + } +} + +#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)] +pub struct Resource { + pub at: usize, + pub location: PathBuf, +} + +impl Resource {} diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index c06badbb26..7ad1c12823 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -15,7 +15,7 @@ impl Command for Get { } fn signature(&self) -> nu_protocol::Signature { - Signature::build("wrap").required( + Signature::build("get").required( "cell_path", SyntaxShape::CellPath, "the cell path to the data", diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index d093cd88ce..3abad8e122 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -3,12 +3,13 @@ use nu_protocol::{ engine::StateWorkingSet, span, DeclId, Span, SyntaxShape, Type, }; +use std::path::Path; use crate::{ lex, lite_parse, parser::{ - check_name, garbage, garbage_statement, parse_block_expression, parse_import_pattern, - parse_internal_call, parse_signature, parse_string, + check_name, garbage, garbage_statement, parse, parse_block_expression, + parse_import_pattern, parse_internal_call, parse_signature, parse_string, }, ParseError, }; @@ -765,3 +766,95 @@ pub fn parse_let( )), ) } + +pub fn parse_source( + working_set: &mut StateWorkingSet, + spans: &[Span], +) -> (Statement, Option) { + let name = working_set.get_span_contents(spans[0]); + + if name == b"source" { + if let Some(decl_id) = working_set.find_decl(b"source") { + // Is this the right call to be using here? + // Some of the others (`parse_let`) use it, some of them (`parse_hide`) don't. + let (call, call_span, err) = + parse_internal_call(working_set, spans[0], &spans[1..], decl_id); + + // Command and one file name + if spans.len() >= 2 { + let name_expr = working_set.get_span_contents(spans[1]); + if let Ok(filename) = String::from_utf8(name_expr.to_vec()) { + let source_file = Path::new(&filename); + + let path = source_file; + let contents = std::fs::read(path); + + if let Ok(contents) = contents { + // This will load the defs from the file into the + // working set, if it was a successful parse. + let (block, err) = parse( + working_set, + path.file_name().and_then(|x| x.to_str()), + &contents, + false, + ); + + if err.is_some() { + // Unsuccessful parse of file + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: span(&spans[1..]), + ty: Type::Unknown, + custom_completion: None, + }])), + // Return the file parse error + err, + ); + } else { + // Save the block into the working set + let block_id = working_set.add_block(block); + + let mut call_with_block = call; + + // Adding this expression to the positional creates a syntax highlighting error + // after writing `source example.nu` + call_with_block.positional.push(Expression { + expr: Expr::Int(block_id as i64), + span: spans[1], + ty: Type::Unknown, + custom_completion: None, + }); + + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call_with_block), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + None, + ); + } + } + } + } + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + err, + ); + } + } + ( + garbage_statement(spans), + Some(ParseError::UnknownState( + "internal error: source statement unparseable".into(), + span(spans), + )), + ) +} diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index e1a4f62867..8cf6351c57 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -1,5 +1,6 @@ use crate::{ lex, lite_parse, + parse_keywords::parse_source, type_check::{math_result_type, type_compatible}, LiteBlock, ParseError, Token, TokenContents, }; @@ -2870,6 +2871,7 @@ pub fn parse_statement( b"alias" => parse_alias(working_set, spans), b"module" => parse_module(working_set, spans), b"use" => parse_use(working_set, spans), + b"source" => parse_source(working_set, spans), b"export" => ( garbage_statement(spans), Some(ParseError::UnexpectedKeyword("export".into(), spans[0])), diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 0bb59291e7..6a7464508e 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -83,10 +83,18 @@ pub enum ShellError { #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] FileNotFound(#[label("file not found")] Span), + #[error("File not found")] + #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] + FileNotFoundCustom(String, #[label("{0}")] Span), + #[error("Directory not found")] #[diagnostic(code(nu::shell::directory_not_found), url(docsrs))] DirectoryNotFound(#[label("directory not found")] Span), + #[error("File not found")] + #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] + DirectoryNotFoundCustom(String, #[label("{0}")] Span), + #[error("Move not possible")] #[diagnostic(code(nu::shell::move_not_possible), url(docsrs))] MoveNotPossible { @@ -97,4 +105,26 @@ pub enum ShellError { #[label("{destination_message}")] destination_span: Span, }, + + #[error("Move not possible")] + #[diagnostic(code(nu::shell::move_not_possible_single), url(docsrs))] + MoveNotPossibleSingle(String, #[label("{0}")] Span), +} + +impl From for ShellError { + fn from(input: std::io::Error) -> ShellError { + ShellError::InternalError(format!("{:?}", input)) + } +} + +impl std::convert::From> for ShellError { + fn from(input: Box) -> ShellError { + ShellError::InternalError(input.to_string()) + } +} + +impl From> for ShellError { + fn from(input: Box) -> ShellError { + ShellError::InternalError(format!("{:?}", input)) + } } diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index 1e634964a4..afc1f90161 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -247,6 +247,7 @@ impl Value { } } + /// Follow a given column path into the value: for example accessing nth elements in a stream or list pub fn follow_cell_path(self, cell_path: &[PathMember]) -> Result { let mut current = self; for member in cell_path { diff --git a/crates/nu-protocol/src/value/range.rs b/crates/nu-protocol/src/value/range.rs index 38fb286ac2..369ff03e87 100644 --- a/crates/nu-protocol/src/value/range.rs +++ b/crates/nu-protocol/src/value/range.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; use std::cmp::Ordering; +/// A Range is an iterator over integers. use crate::{ ast::{RangeInclusion, RangeOperator}, *,