diff --git a/Cargo.lock b/Cargo.lock index b38f438ba4..df79bf294f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1430,6 +1430,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06d198e9919d9822d5f7083ba8530e04de87841eaf21ead9af8f2304efd57c89" +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi", +] + [[package]] name = "itertools" version = "0.10.3" @@ -1890,6 +1899,7 @@ dependencies = [ name = "nu-cli" version = "0.1.0" dependencies = [ + "is_executable", "log", "miette", "nu-ansi-term", @@ -1937,6 +1947,7 @@ dependencies = [ "indexmap", "itertools", "lazy_static", + "log", "lscolors", "md-5", "meval", @@ -1980,6 +1991,7 @@ dependencies = [ "url", "users", "uuid", + "which", "zip", ] diff --git a/Cargo.toml b/Cargo.toml index 6c4862c27e..3d62988315 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,6 +63,7 @@ default = [ "plugin", "inc", "example", + "which" ] stable = ["default"] @@ -80,6 +81,8 @@ wasi = ["inc"] inc = ["nu_plugin_inc"] example = ["nu_plugin_example"] +which = ["nu-command/which"] + # Extra gstat = ["nu_plugin_gstat"] diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 38361ac94f..44496983e1 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -16,3 +16,4 @@ miette = { version = "3.0.0", features = ["fancy"] } thiserror = "1.0.29" reedline = { git = "https://github.com/nushell/reedline", branch = "main" } log = "0.4" +is_executable = "1.0.1" \ No newline at end of file diff --git a/crates/nu-cli/src/completions.rs b/crates/nu-cli/src/completions.rs index 028dd93328..38bf7ccaa6 100644 --- a/crates/nu-cli/src/completions.rs +++ b/crates/nu-cli/src/completions.rs @@ -19,6 +19,170 @@ impl NuCompleter { Self { engine_state } } + fn external_command_completion(&self, prefix: &str) -> Vec { + let mut executables = vec![]; + + let paths; + + #[cfg(windows)] + { + paths = self.engine_state.env_vars.get("Path"); + } + + #[cfg(not(windows))] + { + paths = self.engine_state.env_vars.get("PATH"); + } + + if let Some(paths) = paths { + if let Ok(paths) = paths.as_list() { + for path in paths { + let path = path.as_string().unwrap_or_default(); + + if let Ok(mut contents) = std::fs::read_dir(path) { + while let Some(Ok(item)) = contents.next() { + if !executables.contains( + &item + .path() + .file_name() + .map(|x| x.to_string_lossy().to_string()) + .unwrap_or_default(), + ) && matches!( + item.path() + .file_name() + .map(|x| x.to_string_lossy().starts_with(prefix)), + Some(true) + ) && is_executable::is_executable(&item.path()) + { + if let Ok(name) = item.file_name().into_string() { + executables.push(name); + } + } + } + } + } + } + } + + executables + } + + fn complete_variables( + &self, + working_set: &StateWorkingSet, + prefix: &[u8], + span: Span, + offset: usize, + ) -> Vec<(reedline::Span, String)> { + let mut output = vec![]; + + let builtins = ["$nu", "$scope", "$in", "$config", "$env"]; + + for builtin in builtins { + if builtin.as_bytes().starts_with(prefix) { + output.push(( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + builtin.to_string(), + )); + } + } + + for scope in &working_set.delta.scope { + for v in &scope.vars { + if v.0.starts_with(prefix) { + output.push(( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + String::from_utf8_lossy(v.0).to_string(), + )); + } + } + } + for scope in &self.engine_state.scope { + for v in &scope.vars { + if v.0.starts_with(prefix) { + output.push(( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + String::from_utf8_lossy(v.0).to_string(), + )); + } + } + } + + output.dedup(); + + output + } + + fn complete_filepath_and_commands( + &self, + working_set: &StateWorkingSet, + span: Span, + offset: usize, + ) -> Vec<(reedline::Span, String)> { + let prefix = working_set.get_span_contents(span); + + let results = working_set + .find_commands_by_prefix(prefix) + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + String::from_utf8_lossy(&x).to_string(), + ) + }); + let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") { + match d.as_string() { + Ok(s) => s, + Err(_) => "".to_string(), + } + } else { + "".to_string() + }; + + let prefix = String::from_utf8_lossy(prefix).to_string(); + let results_paths = file_path_completion(span, &prefix, &cwd) + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: x.0.start - offset, + end: x.0.end - offset, + }, + x.1, + ) + }); + + let results_external = + self.external_command_completion(&prefix) + .into_iter() + .map(move |x| { + ( + reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + x, + ) + }); + + results + .chain(results_paths.into_iter()) + .chain(results_external.into_iter()) + .collect() + } + fn completion_helper(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> { let mut working_set = StateWorkingSet::new(&self.engine_state); let offset = working_set.next_span_start(); @@ -28,35 +192,20 @@ impl NuCompleter { for stmt in output.stmts.into_iter() { if let Statement::Pipeline(pipeline) = stmt { for expr in pipeline.expressions { - if pos >= expr.span.start - && (pos <= (line.len() + offset) || pos <= expr.span.end) - { - let possible_cmd = working_set.get_span_contents(Span { - start: expr.span.start, - end: pos, - }); - - let results = working_set.find_commands_by_prefix(possible_cmd); - - if !results.is_empty() { - return results - .into_iter() - .map(move |x| { - ( - reedline::Span { - start: expr.span.start - offset, - end: pos - offset, - }, - String::from_utf8_lossy(&x).to_string(), - ) - }) - .collect(); - } - } - let flattened = flatten_expression(&working_set, &expr); for flat in flattened { if pos >= flat.0.start && pos <= flat.0.end { + let prefix = working_set.get_span_contents(flat.0); + + if prefix.starts_with(b"$") { + return self.complete_variables( + &working_set, + prefix, + flat.0, + offset, + ); + } + match &flat.1 { nu_parser::FlatShape::Custom(custom_completion) => { let prefix = working_set.get_span_contents(flat.0).to_vec(); @@ -81,8 +230,8 @@ impl NuCompleter { .into_iter() .map(move |x| { let s = x.as_string().expect( - "FIXME: better error handling for custom completions", - ); + "FIXME: better error handling for custom completions", + ); ( reedline::Span { @@ -102,44 +251,11 @@ impl NuCompleter { nu_parser::FlatShape::External | nu_parser::FlatShape::InternalCall | nu_parser::FlatShape::String => { - let prefix = working_set.get_span_contents(flat.0); - let results = working_set.find_commands_by_prefix(prefix); - let cwd = if let Some(d) = self.engine_state.env_vars.get("PWD") - { - match d.as_string() { - Ok(s) => s, - Err(_) => "".to_string(), - } - } else { - "".to_string() - }; - - let prefix = String::from_utf8_lossy(prefix).to_string(); - let results2 = file_path_completion(flat.0, &prefix, &cwd) - .into_iter() - .map(move |x| { - ( - reedline::Span { - start: x.0.start - offset, - end: x.0.end - offset, - }, - x.1, - ) - }); - - return results - .into_iter() - .map(move |x| { - ( - reedline::Span { - start: flat.0.start - offset, - end: flat.0.end - offset, - }, - String::from_utf8_lossy(&x).to_string(), - ) - }) - .chain(results2.into_iter()) - .collect(); + return self.complete_filepath_and_commands( + &working_set, + flat.0, + offset, + ); } nu_parser::FlatShape::Filepath | nu_parser::FlatShape::GlobPattern @@ -171,41 +287,7 @@ impl NuCompleter { }) .collect(); } - _ => { - let prefix = working_set.get_span_contents(flat.0); - - if prefix.starts_with(b"$") { - let mut output = vec![]; - - for scope in &working_set.delta.scope { - for v in &scope.vars { - if v.0.starts_with(prefix) { - output.push(( - reedline::Span { - start: flat.0.start - offset, - end: flat.0.end - offset, - }, - String::from_utf8_lossy(v.0).to_string(), - )); - } - } - } - for scope in &self.engine_state.scope { - for v in &scope.vars { - if v.0.starts_with(prefix) { - output.push(( - reedline::Span { - start: flat.0.start - offset, - end: flat.0.end - offset, - }, - String::from_utf8_lossy(v.0).to_string(), - )); - } - } - } - return output; - } - } + _ => {} } } } diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 2baeba13ad..c81b5f9556 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -72,6 +72,8 @@ encoding_rs = "0.8.30" num = { version = "0.4.0", optional = true } reqwest = {version = "0.11", features = ["blocking"] } mime = "0.3.16" +log = "0.4.14" +which = { version = "4.2.2", optional = true } [target.'cfg(unix)'.dependencies] umask = "1.0.0" diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index f6b8617e9c..6a925eee54 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -112,6 +112,9 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { Sys, }; + #[cfg(feature = "which")] + bind_command! { Which }; + // Strings bind_command! { BuildString, diff --git a/crates/nu-command/src/system/mod.rs b/crates/nu-command/src/system/mod.rs index a921225973..3225ddde7b 100644 --- a/crates/nu-command/src/system/mod.rs +++ b/crates/nu-command/src/system/mod.rs @@ -2,8 +2,10 @@ mod benchmark; mod ps; mod run_external; mod sys; +mod which_; pub use benchmark::Benchmark; pub use ps::Ps; pub use run_external::{External, ExternalCommand}; pub use sys::Sys; +pub use which_::Which; diff --git a/crates/nu-command/src/system/which_.rs b/crates/nu-command/src/system/which_.rs new file mode 100644 index 0000000000..529a38f1d3 --- /dev/null +++ b/crates/nu-command/src/system/which_.rs @@ -0,0 +1,256 @@ +use itertools::Itertools; +use log::trace; +use nu_engine::CallExt; +use nu_protocol::{ + ast::Call, + engine::{Command, EngineState, Stack}, + Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span, + Spanned, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Which; + +impl Command for Which { + fn name(&self) -> &str { + "which" + } + + fn signature(&self) -> Signature { + Signature::build("which") + .required("application", SyntaxShape::String, "application") + .rest("rest", SyntaxShape::String, "additional applications") + .switch("all", "list all executables", Some('a')) + .category(Category::System) + } + + fn usage(&self) -> &str { + "Finds a program file, alias or custom command." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + which(engine_state, stack, call) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Find if the 'myapp' application is available", + example: "which myapp", + result: None, + }] + } +} + +/// Shortcuts for creating an entry to the output table +fn entry(arg: impl Into, path: Value, builtin: bool, span: Span) -> Value { + let mut cols = vec![]; + let mut vals = vec![]; + + cols.push("arg".to_string()); + vals.push(Value::string(arg.into(), span)); + + cols.push("path".to_string()); + vals.push(path); + + cols.push("builtin".to_string()); + vals.push(Value::Bool { val: builtin, span }); + + Value::Record { cols, vals, span } +} + +macro_rules! create_entry { + ($arg:expr, $path:expr, $span:expr, $is_builtin:expr) => { + entry( + $arg.clone(), + Value::string($path.to_string(), $span), + $is_builtin, + $span, + ) + }; +} + +fn get_entries_in_aliases(engine_state: &EngineState, name: &str, span: Span) -> Vec { + let aliases = engine_state.find_aliases(name); + + let aliases = aliases + .into_iter() + .map(|spans| { + spans + .into_iter() + .map(|span| { + String::from_utf8_lossy(engine_state.get_span_contents(&span)).to_string() + }) + .join(" ") + }) + .map(|alias| create_entry!(name, format!("Nushell alias: {}", alias), span, false)) + .collect::>(); + trace!("Found {} aliases", aliases.len()); + aliases +} + +fn get_entries_in_custom_command(engine_state: &EngineState, name: &str, span: Span) -> Vec { + let custom_commands = engine_state.find_custom_commands(name); + + custom_commands + .into_iter() + .map(|_| create_entry!(name, "Nushell custom command", span, false)) + .collect::>() +} + +fn get_entry_in_commands(engine_state: &EngineState, name: &str, span: Span) -> Option { + if engine_state.find_decl(name.as_bytes()).is_some() { + Some(create_entry!(name, "Nushell built-in command", span, true)) + } else { + None + } +} + +fn get_entries_in_nu( + engine_state: &EngineState, + name: &str, + span: Span, + skip_after_first_found: bool, +) -> Vec { + let mut all_entries = vec![]; + + all_entries.extend(get_entries_in_aliases(engine_state, name, span)); + if !all_entries.is_empty() && skip_after_first_found { + return all_entries; + } + + all_entries.extend(get_entries_in_custom_command(engine_state, name, span)); + if !all_entries.is_empty() && skip_after_first_found { + return all_entries; + } + + if let Some(entry) = get_entry_in_commands(engine_state, name, span) { + all_entries.push(entry); + } + + all_entries +} + +#[allow(unused)] +macro_rules! entry_path { + ($arg:expr, $path:expr, $span:expr) => { + entry($arg.clone(), Value::string($path, $span), false, $span) + }; +} + +#[cfg(feature = "which")] +fn get_first_entry_in_path(item: &str, span: Span) -> Option { + which::which(item) + .map(|path| entry_path!(item, path.to_string_lossy().to_string(), span)) + .ok() +} + +#[cfg(not(feature = "which"))] +fn get_first_entry_in_path(_: &str, _: Span) -> Option { + None +} + +#[cfg(feature = "which")] +fn get_all_entries_in_path(item: &str, span: Span) -> Vec { + which::which_all(&item) + .map(|iter| { + iter.map(|path| entry_path!(item, path.to_string_lossy().to_string(), span)) + .collect() + }) + .unwrap_or_default() +} +#[cfg(not(feature = "which"))] +fn get_all_entries_in_path(_: &str, _: Span) -> Vec { + vec![] +} + +#[derive(Debug)] +struct WhichArgs { + applications: Vec>, + all: bool, +} + +fn which_single(application: Spanned, all: bool, engine_state: &EngineState) -> Vec { + let (external, prog_name) = if application.item.starts_with('^') { + (true, application.item[1..].to_string()) + } else { + (false, application.item.clone()) + }; + + //If prog_name is an external command, don't search for nu-specific programs + //If all is false, we can save some time by only searching for the first matching + //program + //This match handles all different cases + match (all, external) { + (true, true) => get_all_entries_in_path(&prog_name, application.span), + (true, false) => { + let mut output: Vec = vec![]; + output.extend(get_entries_in_nu( + engine_state, + &prog_name, + application.span, + false, + )); + output.extend(get_all_entries_in_path(&prog_name, application.span)); + output + } + (false, true) => { + if let Some(entry) = get_first_entry_in_path(&prog_name, application.span) { + return vec![entry]; + } + vec![] + } + (false, false) => { + let nu_entries = get_entries_in_nu(engine_state, &prog_name, application.span, true); + if !nu_entries.is_empty() { + return vec![nu_entries[0].clone()]; + } else if let Some(entry) = get_first_entry_in_path(&prog_name, application.span) { + return vec![entry]; + } + vec![] + } + } +} + +fn which( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, +) -> Result { + let which_args = WhichArgs { + applications: call.rest(engine_state, stack, 0)?, + all: call.has_flag("all"), + }; + let ctrlc = engine_state.ctrlc.clone(); + + if which_args.applications.is_empty() { + return Err(ShellError::MissingParameter( + "application".into(), + call.head, + )); + } + + let mut output = vec![]; + + for app in which_args.applications { + let values = which_single(app, which_args.all, engine_state); + output.extend(values); + } + + Ok(output.into_iter().into_pipeline_data(ctrlc)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + crate::test_examples(Which) + } +} diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 26f2adae99..7377feaa41 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -344,6 +344,34 @@ impl EngineState { } } + pub fn find_aliases(&self, name: &str) -> Vec> { + let mut output = vec![]; + + for frame in &self.scope { + if let Some(alias) = frame.aliases.get(name.as_bytes()) { + output.push(alias.clone()); + } + } + + output + } + + pub fn find_custom_commands(&self, name: &str) -> Vec { + let mut output = vec![]; + + for frame in &self.scope { + if let Some(decl_id) = frame.decls.get(name.as_bytes()) { + let decl = self.get_decl(*decl_id); + + if let Some(block_id) = decl.get_block_id() { + output.push(self.get_block(block_id).clone()); + } + } + } + + output + } + pub fn find_decl(&self, name: &[u8]) -> Option { let mut visibility: Visibility = Visibility::new();