Skip to main content

nu_command/system/
which_.rs

1use nu_engine::{command_prelude::*, env};
2use nu_protocol::engine::CommandType;
3use std::collections::HashSet;
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/// Returns the source file path that covers `span`, if any.
68fn file_for_span(engine_state: &EngineState, span: Span) -> Option<String> {
69    engine_state
70        .files()
71        .find(|f| f.covered_span.contains_span(span))
72        .map(|f| f.name.to_string())
73}
74
75/// Returns the source file path for a declaration, if it can be determined.
76///
77/// - Aliases: resolved via `decl_span()` (the alias expansion span)
78/// - Custom commands: resolved from the block's span via `block_id()`
79/// - Plugins: resolved from the plugin identity's filename
80/// - Known externals (`extern` declarations): resolved via `decl_span()`
81fn file_for_decl(
82    engine_state: &EngineState,
83    decl: &dyn nu_protocol::engine::Command,
84) -> Option<String> {
85    if let Some(block_id) = decl.block_id() {
86        return engine_state
87            .get_block(block_id)
88            .span
89            .and_then(|sp| file_for_span(engine_state, sp));
90    }
91    #[cfg(feature = "plugin")]
92    if decl.is_plugin() {
93        return decl
94            .plugin_identity()
95            .map(|id| id.filename().to_string_lossy().to_string());
96    }
97    if let Some(span) = decl.decl_span() {
98        return file_for_span(engine_state, span);
99    }
100    None
101}
102
103// Shortcut for creating an entry to the output table.
104fn entry(
105    arg: impl Into<String>,
106    path: impl Into<String>,
107    cmd_type: CommandType,
108    definition: Option<String>,
109    file: Option<String>,
110    span: Span,
111) -> Value {
112    let arg = arg.into();
113    let path = path.into();
114    let path_value = if path.is_empty() {
115        file.unwrap_or_default()
116    } else {
117        path.clone()
118    };
119
120    let mut record = record! {
121        "command" => Value::string(arg, span),
122        "path" => Value::string(path_value, span),
123        "type" => Value::string(cmd_type.to_string(), span),
124    };
125
126    if let Some(def) = definition {
127        record.insert("definition", Value::string(def, span));
128    }
129
130    Value::record(record, span)
131}
132
133fn get_entry_in_commands(engine_state: &EngineState, name: &str, span: Span) -> Option<Value> {
134    let decl_id = engine_state.find_decl(name.as_bytes(), &[])?;
135    let decl = engine_state.get_decl(decl_id);
136    let definition = if decl.command_type() == CommandType::Alias {
137        decl.as_alias().map(|alias| {
138            String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
139                .to_string()
140        })
141    } else {
142        None
143    };
144    let file = file_for_decl(engine_state, decl);
145    Some(entry(name, "", decl.command_type(), definition, file, span))
146}
147
148fn get_first_entry_in_path(
149    item: &str,
150    span: Span,
151    cwd: impl AsRef<Path>,
152    paths: impl AsRef<OsStr>,
153) -> Option<Value> {
154    which::which_in(item, Some(paths), cwd)
155        .map(|path| {
156            let full_path = path.to_string_lossy().to_string();
157            entry(
158                item,
159                full_path.clone(),
160                CommandType::External,
161                None,
162                Some(full_path),
163                span,
164            )
165        })
166        .ok()
167}
168
169fn get_all_entries_in_path(
170    item: &str,
171    span: Span,
172    cwd: impl AsRef<Path>,
173    paths: impl AsRef<OsStr>,
174) -> Vec<Value> {
175    // `which_in_all` canonicalizes every result path. On systems where PATH
176    // contains both a real directory and a symlink pointing to the same place
177    // (e.g. `/usr/bin` and `/bin -> /usr/bin` on WSL/Debian), the same
178    // canonical path would appear multiple times. The HashSet deduplicates
179    // those before we build the output rows.
180    let mut seen = HashSet::new();
181    which::which_in_all(item, Some(paths), cwd)
182        .map(|iter| {
183            iter.filter(|path| seen.insert(path.clone()))
184                .map(|path| {
185                    let full_path = path.to_string_lossy().to_string();
186                    entry(
187                        item,
188                        full_path.clone(),
189                        CommandType::External,
190                        None,
191                        Some(full_path),
192                        span,
193                    )
194                })
195                .collect()
196        })
197        .unwrap_or_default()
198}
199
200fn list_all_executables(
201    engine_state: &EngineState,
202    paths: impl AsRef<OsStr>,
203    all: bool,
204) -> Vec<Value> {
205    let decls = engine_state.get_decls_sorted(false);
206
207    let mut results = Vec::with_capacity(decls.len());
208    let mut seen_commands = HashSet::with_capacity(decls.len());
209
210    for (name_bytes, decl_id) in decls {
211        let name = String::from_utf8_lossy(&name_bytes).to_string();
212        seen_commands.insert(name.clone());
213        let decl = engine_state.get_decl(decl_id);
214        let definition = if decl.command_type() == CommandType::Alias {
215            decl.as_alias().map(|alias| {
216                String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
217                    .to_string()
218            })
219        } else {
220            None
221        };
222        let file = file_for_decl(engine_state, decl);
223
224        results.push(entry(
225            name,
226            String::new(),
227            decl.command_type(),
228            definition,
229            file,
230            Span::unknown(),
231        ));
232    }
233
234    // Add PATH executables
235    let path_iter = sys::RealSys
236        .env_split_paths(paths.as_ref())
237        .into_iter()
238        .filter_map(|dir| fs::read_dir(dir).ok())
239        .flat_map(|entries| entries.flatten())
240        .map(|entry| entry.path())
241        .filter_map(|path| {
242            if !path.is_executable() {
243                return None;
244            }
245            let filename = path.file_name()?.to_string_lossy().to_string();
246
247            if !all && !seen_commands.insert(filename.clone()) {
248                return None;
249            }
250
251            let full_path = path.to_string_lossy().to_string();
252            Some(entry(
253                filename,
254                full_path.clone(),
255                CommandType::External,
256                None,
257                Some(full_path),
258                Span::unknown(),
259            ))
260        });
261
262    results.extend(path_iter);
263    results
264}
265
266#[derive(Debug)]
267struct WhichArgs {
268    applications: Vec<Spanned<String>>,
269    all: bool,
270}
271
272fn which_single(
273    application: Spanned<String>,
274    all: bool,
275    engine_state: &EngineState,
276    cwd: impl AsRef<Path>,
277    paths: impl AsRef<OsStr>,
278) -> Vec<Value> {
279    let cwd = cwd.as_ref();
280    let paths = paths.as_ref();
281    let (external, prog_name) = if application.item.starts_with('^') {
282        (true, application.item[1..].to_string())
283    } else {
284        (false, application.item.clone())
285    };
286
287    // If prog_name is an external command, don't search for nu-specific programs.
288    // If all is false, we can save some time by only searching for the first match.
289    match (all, external) {
290        (true, true) => get_all_entries_in_path(&prog_name, application.span, cwd, paths),
291        (true, false) => {
292            let mut output: Vec<Value> = vec![];
293            if let Some(entry) = get_entry_in_commands(engine_state, &prog_name, application.span) {
294                output.push(entry);
295            }
296            output.extend(get_all_entries_in_path(
297                &prog_name,
298                application.span,
299                cwd,
300                paths,
301            ));
302            output
303        }
304        (false, true) => get_first_entry_in_path(&prog_name, application.span, cwd, paths)
305            .into_iter()
306            .collect(),
307        (false, false) => get_entry_in_commands(engine_state, &prog_name, application.span)
308            .or_else(|| get_first_entry_in_path(&prog_name, application.span, cwd, paths))
309            .into_iter()
310            .collect(),
311    }
312}
313
314fn which(
315    engine_state: &EngineState,
316    stack: &mut Stack,
317    call: &Call,
318) -> Result<PipelineData, ShellError> {
319    let head = call.head;
320    let which_args = WhichArgs {
321        applications: call.rest(engine_state, stack, 0)?,
322        all: call.has_flag(engine_state, stack, "all")?,
323    };
324
325    let mut output = vec![];
326
327    let cwd = engine_state.cwd_as_string(Some(stack))?;
328
329    // PATH may not be set in minimal environments (e.g. plugin test harnesses).
330    // In that case we can still resolve built-ins, aliases, custom commands and
331    // known externals; we just won't find any PATH-based binaries.
332    let paths = env::path_str(engine_state, stack, head).unwrap_or_default();
333
334    if which_args.applications.is_empty() {
335        return Ok(list_all_executables(engine_state, &paths, which_args.all)
336            .into_iter()
337            .into_pipeline_data(head, engine_state.signals().clone()));
338    }
339
340    for app in which_args.applications {
341        let values = which_single(app, which_args.all, engine_state, &cwd, &paths);
342        output.extend(values);
343    }
344
345    Ok(output
346        .into_iter()
347        .into_pipeline_data(head, engine_state.signals().clone()))
348}
349
350#[cfg(test)]
351mod test {
352    use super::*;
353
354    #[test]
355    fn test_examples() {
356        crate::test_examples(Which)
357    }
358}
359
360// --------------------
361// Copied from https://docs.rs/is_executable/ v1.0.5
362// Removed path.exists() check in `mod windows`.
363
364/// An extension trait for `std::fs::Path` providing an `is_executable` method.
365///
366/// See the module documentation for examples.
367pub trait IsExecutable {
368    /// Returns `true` if there is a file at the given path and it is
369    /// executable. Returns `false` otherwise.
370    ///
371    /// See the module documentation for details.
372    fn is_executable(&self) -> bool;
373}
374
375#[cfg(unix)]
376mod unix {
377    use std::os::unix::fs::PermissionsExt;
378    use std::path::Path;
379
380    use super::IsExecutable;
381
382    impl IsExecutable for Path {
383        fn is_executable(&self) -> bool {
384            let metadata = match self.metadata() {
385                Ok(metadata) => metadata,
386                Err(_) => return false,
387            };
388            let permissions = metadata.permissions();
389            metadata.is_file() && permissions.mode() & 0o111 != 0
390        }
391    }
392}
393
394#[cfg(target_os = "windows")]
395mod windows {
396    use std::os::windows::ffi::OsStrExt;
397    use std::path::Path;
398
399    use windows::Win32::Storage::FileSystem::GetBinaryTypeW;
400    use windows::core::PCWSTR;
401
402    use super::IsExecutable;
403
404    impl IsExecutable for Path {
405        fn is_executable(&self) -> bool {
406            // Check using file extension
407            if let Some(pathext) = std::env::var_os("PATHEXT")
408                && let Some(extension) = self.extension()
409            {
410                let extension = extension.to_string_lossy();
411
412                // Originally taken from:
413                // https://github.com/nushell/nushell/blob/93e8f6c05e1e1187d5b674d6b633deb839c84899/crates/nu-cli/src/completion/command.rs#L64-L74
414                return pathext
415                    .to_string_lossy()
416                    .split(';')
417                    // Filter out empty tokens and ';' at the end
418                    .filter(|f| f.len() > 1)
419                    .any(|ext| {
420                        // Cut off the leading '.' character
421                        let ext = &ext[1..];
422                        extension.eq_ignore_ascii_case(ext)
423                    });
424            }
425
426            // Check using file properties
427            // This code is only reached if there is no file extension or retrieving PATHEXT fails
428            let windows_string: Vec<u16> = self.as_os_str().encode_wide().chain(Some(0)).collect();
429            let mut binary_type: u32 = 0;
430
431            let result =
432                unsafe { GetBinaryTypeW(PCWSTR(windows_string.as_ptr()), &mut binary_type) };
433            if result.is_ok()
434                && let 0..=6 = binary_type
435            {
436                return true;
437            }
438
439            false
440        }
441    }
442}
443
444// For WASI, we can't check if a file is executable
445// Since wasm and wasi
446//  is not supposed to add executables ideologically,
447// specify them collectively
448#[cfg(any(target_os = "wasi", target_family = "wasm"))]
449mod wasm {
450    use std::path::Path;
451
452    use super::IsExecutable;
453
454    impl IsExecutable for Path {
455        fn is_executable(&self) -> bool {
456            false
457        }
458    }
459}