nu_command/system/
which_.rs

1use itertools::Itertools;
2use nu_engine::{command_prelude::*, env};
3use nu_protocol::engine::CommandType;
4use std::fs;
5use std::{ffi::OsStr, path::Path};
6use which::sys;
7use which::sys::Sys;
8
9#[derive(Clone)]
10pub struct Which;
11
12impl Command for Which {
13    fn name(&self) -> &str {
14        "which"
15    }
16
17    fn signature(&self) -> Signature {
18        Signature::build("which")
19            .input_output_types(vec![(Type::Nothing, Type::table())])
20            .allow_variants_without_examples(true)
21            .rest("applications", SyntaxShape::String, "Application(s).")
22            .switch("all", "list all executables", Some('a'))
23            .category(Category::System)
24    }
25
26    fn description(&self) -> &str {
27        "Finds a program file, alias or custom command. If `application` is not provided, all deduplicated commands will be returned."
28    }
29
30    fn search_terms(&self) -> Vec<&str> {
31        vec![
32            "find",
33            "path",
34            "location",
35            "command",
36            "whereis",     // linux binary to find binary locations in path
37            "get-command", // powershell command to find commands and binaries in path
38        ]
39    }
40
41    fn run(
42        &self,
43        engine_state: &EngineState,
44        stack: &mut Stack,
45        call: &Call,
46        _input: PipelineData,
47    ) -> Result<PipelineData, ShellError> {
48        which(engine_state, stack, call)
49    }
50
51    fn examples(&self) -> Vec<Example<'_>> {
52        vec![
53            Example {
54                description: "Find if the 'myapp' application is available",
55                example: "which myapp",
56                result: None,
57            },
58            Example {
59                description: "Find all executables across all paths without deduplication",
60                example: "which -a",
61                result: None,
62            },
63        ]
64    }
65}
66
67// Shortcut for creating an entry to the output table
68fn entry(
69    arg: impl Into<String>,
70    path: impl Into<String>,
71    cmd_type: CommandType,
72    span: Span,
73) -> Value {
74    Value::record(
75        record! {
76            "command" => Value::string(arg, span),
77            "path" => Value::string(path, span),
78            "type" => Value::string(cmd_type.to_string(), span),
79        },
80        span,
81    )
82}
83
84fn get_entry_in_commands(engine_state: &EngineState, name: &str, span: Span) -> Option<Value> {
85    if let Some(decl_id) = engine_state.find_decl(name.as_bytes(), &[]) {
86        let decl = engine_state.get_decl(decl_id);
87        Some(entry(name, "", decl.command_type(), span))
88    } else {
89        None
90    }
91}
92
93fn get_first_entry_in_path(
94    item: &str,
95    span: Span,
96    cwd: impl AsRef<Path>,
97    paths: impl AsRef<OsStr>,
98) -> Option<Value> {
99    which::which_in(item, Some(paths), cwd)
100        .map(|path| entry(item, path.to_string_lossy(), CommandType::External, span))
101        .ok()
102}
103
104fn get_all_entries_in_path(
105    item: &str,
106    span: Span,
107    cwd: impl AsRef<Path>,
108    paths: impl AsRef<OsStr>,
109) -> Vec<Value> {
110    which::which_in_all(&item, Some(paths), cwd)
111        .map(|iter| {
112            iter.map(|path| entry(item, path.to_string_lossy(), CommandType::External, span))
113                .collect()
114        })
115        .unwrap_or_default()
116}
117
118fn list_all_executables(
119    engine_state: &EngineState,
120    paths: impl AsRef<OsStr>,
121    all: bool,
122) -> Vec<Value> {
123    let decls = engine_state.get_decls_sorted(false);
124    let commands = decls
125        .into_iter()
126        .map(|x| {
127            let decl = engine_state.get_decl(x.1);
128            (
129                String::from_utf8_lossy(&x.0).to_string(),
130                String::new(),
131                decl.command_type(),
132            )
133        })
134        .chain(
135            sys::RealSys
136                .env_split_paths(paths.as_ref())
137                .into_iter()
138                .filter_map(|dir| fs::read_dir(dir).ok())
139                .flat_map(|entries| entries.flatten())
140                .map(|entry| entry.path())
141                .filter(|path| path.is_file())
142                .filter_map(|path| {
143                    let filename = path.file_name()?.to_string_lossy().to_string();
144                    Some((
145                        filename,
146                        path.to_string_lossy().to_string(),
147                        CommandType::External,
148                    ))
149                }),
150        );
151
152    if all {
153        commands
154            .map(|(filename, path, cmd_type)| entry(filename, path, cmd_type, Span::new(0, 0)))
155            .collect()
156    } else {
157        commands
158            .unique_by(|x| x.0.clone())
159            .map(|(filename, path, cmd_type)| entry(filename, path, cmd_type, Span::new(0, 0)))
160            .collect()
161    }
162}
163
164#[derive(Debug)]
165struct WhichArgs {
166    applications: Vec<Spanned<String>>,
167    all: bool,
168}
169
170fn which_single(
171    application: Spanned<String>,
172    all: bool,
173    engine_state: &EngineState,
174    cwd: impl AsRef<Path>,
175    paths: impl AsRef<OsStr>,
176) -> Vec<Value> {
177    let (external, prog_name) = if application.item.starts_with('^') {
178        (true, application.item[1..].to_string())
179    } else {
180        (false, application.item.clone())
181    };
182
183    //If prog_name is an external command, don't search for nu-specific programs
184    //If all is false, we can save some time by only searching for the first matching
185    //program
186    //This match handles all different cases
187    match (all, external) {
188        (true, true) => get_all_entries_in_path(&prog_name, application.span, cwd, paths),
189        (true, false) => {
190            let mut output: Vec<Value> = vec![];
191            if let Some(entry) = get_entry_in_commands(engine_state, &prog_name, application.span) {
192                output.push(entry);
193            }
194            output.extend(get_all_entries_in_path(
195                &prog_name,
196                application.span,
197                cwd,
198                paths,
199            ));
200            output
201        }
202        (false, true) => get_first_entry_in_path(&prog_name, application.span, cwd, paths)
203            .into_iter()
204            .collect(),
205        (false, false) => get_entry_in_commands(engine_state, &prog_name, application.span)
206            .or_else(|| get_first_entry_in_path(&prog_name, application.span, cwd, paths))
207            .into_iter()
208            .collect(),
209    }
210}
211
212fn which(
213    engine_state: &EngineState,
214    stack: &mut Stack,
215    call: &Call,
216) -> Result<PipelineData, ShellError> {
217    let head = call.head;
218    let which_args = WhichArgs {
219        applications: call.rest(engine_state, stack, 0)?,
220        all: call.has_flag(engine_state, stack, "all")?,
221    };
222
223    let mut output = vec![];
224
225    #[allow(deprecated)]
226    let cwd = env::current_dir_str(engine_state, stack)?;
227    let paths = env::path_str(engine_state, stack, head)?;
228
229    if which_args.applications.is_empty() {
230        return Ok(list_all_executables(engine_state, paths, which_args.all)
231            .into_iter()
232            .into_pipeline_data(head, engine_state.signals().clone()));
233    }
234
235    for app in which_args.applications {
236        let values = which_single(
237            app,
238            which_args.all,
239            engine_state,
240            cwd.clone(),
241            paths.clone(),
242        );
243        output.extend(values);
244    }
245
246    Ok(output
247        .into_iter()
248        .into_pipeline_data(head, engine_state.signals().clone()))
249}
250
251#[cfg(test)]
252mod test {
253    use super::*;
254
255    #[test]
256    fn test_examples() {
257        crate::test_examples(Which)
258    }
259}