Skip to main content

nu_command/system/
which_.rs

1use nu_engine::{command_prelude::*, env};
2use nu_protocol::PipelineMetadata;
3use nu_protocol::engine::CommandType;
4use std::collections::HashSet;
5use std::ffi::{OsStr, OsString};
6use std::fs;
7use std::path::{Path, PathBuf};
8use which::WhichConfig;
9use which::sys::{RealSys, Sys};
10
11#[derive(Clone)]
12pub struct Which;
13
14impl Command for Which {
15    fn name(&self) -> &str {
16        "which"
17    }
18
19    fn signature(&self) -> Signature {
20        Signature::build("which")
21            .input_output_types(vec![(Type::Nothing, Type::table())])
22            .allow_variants_without_examples(true)
23            .param(Parameter::Rest(
24                PositionalArg::new("applications", SyntaxShape::String)
25                    .desc("Application(s).")
26                    .completion(Completion::Builtin(BuiltinCompletion::Command {
27                        internal_only: false,
28                    })),
29            ))
30            .switch("all", "List all executables.", Some('a'))
31            .category(Category::System)
32    }
33
34    fn description(&self) -> &str {
35        "Finds a program file, alias or custom command. If `application` is not provided, all deduplicated commands will be returned."
36    }
37
38    fn search_terms(&self) -> Vec<&str> {
39        vec![
40            "find",
41            "path",
42            "location",
43            "command",
44            "whereis",     // linux binary to find binary locations in path
45            "get-command", // powershell command to find commands and binaries in path
46        ]
47    }
48
49    fn run(
50        &self,
51        engine_state: &EngineState,
52        stack: &mut Stack,
53        call: &Call,
54        _input: PipelineData,
55    ) -> Result<PipelineData, ShellError> {
56        which(engine_state, stack, call)
57    }
58
59    fn examples(&self) -> Vec<Example<'_>> {
60        vec![
61            Example {
62                description: "Find if the 'myapp' application is available",
63                example: "which myapp",
64                result: None,
65            },
66            Example {
67                description: "Find all executables across all paths without deduplication",
68                example: "which -a",
69                result: None,
70            },
71        ]
72    }
73}
74
75/// Returns the source file path that covers `span`, if any.
76fn file_for_span(engine_state: &EngineState, span: Span) -> Option<String> {
77    engine_state
78        .files()
79        .find(|f| f.covered_span.contains_span(span))
80        .map(|f| f.name.to_string())
81}
82
83/// Returns the source file path for a declaration, if it can be determined.
84///
85/// - Aliases: resolved via `decl_span()` (the alias expansion span)
86/// - Custom commands: resolved from the block's span via `block_id()`
87/// - Plugins: resolved from the plugin identity's filename
88/// - Known externals (`extern` declarations): resolved via `decl_span()`
89fn file_for_decl(
90    engine_state: &EngineState,
91    decl: &dyn nu_protocol::engine::Command,
92) -> Option<String> {
93    if let Some(block_id) = decl.block_id() {
94        return engine_state
95            .get_block(block_id)
96            .span
97            .and_then(|sp| file_for_span(engine_state, sp));
98    }
99    #[cfg(feature = "plugin")]
100    if decl.is_plugin() {
101        return decl
102            .plugin_identity()
103            .map(|id| id.filename().to_string_lossy().to_string());
104    }
105    if let Some(span) = decl.decl_span() {
106        return file_for_span(engine_state, span);
107    }
108    None
109}
110
111// Shortcut for creating an entry to the output table.
112fn entry(
113    arg: impl Into<String>,
114    path: impl Into<String>,
115    cmd_type: CommandType,
116    definition: Option<String>,
117    file: Option<String>,
118    span: Span,
119) -> Value {
120    let arg = arg.into();
121    let path = path.into();
122    let path_value = if path.is_empty() {
123        file.unwrap_or_default()
124    } else {
125        path.clone()
126    };
127
128    let mut record = record! {
129        "command" => Value::string(arg, span),
130        "path" => Value::string(path_value, span),
131        "type" => Value::string(cmd_type.to_string(), span),
132    };
133
134    if let Some(def) = definition {
135        record.insert("definition", Value::string(def, span));
136    }
137
138    Value::record(record, span)
139}
140
141fn get_entry_in_commands(engine_state: &EngineState, name: &str, span: Span) -> Option<Value> {
142    let decl_id = engine_state.find_decl(name.as_bytes(), &[])?;
143    let decl = engine_state.get_decl(decl_id);
144    let definition = if decl.command_type() == CommandType::Alias {
145        decl.as_alias().map(|alias| {
146            String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
147                .to_string()
148        })
149    } else {
150        None
151    };
152    let file = file_for_decl(engine_state, decl);
153    Some(entry(name, "", decl.command_type(), definition, file, span))
154}
155
156/// Reads `$env.PATHEXT` from the shell environment as an `OsString`, mirroring
157/// how `PATH` is read for lookups. The lookup is case-insensitive, matching the
158/// usual `PATHEXT` casing on Windows.
159///
160/// When the shell environment has no visible `PATHEXT`, a value that was
161/// explicitly hidden (e.g. `hide-env PATHEXT`) stays hidden: `None` is
162/// returned rather than resurrecting the hidden value from the process
163/// environment. Only when the shell has never seen `PATHEXT` do we fall back
164/// to the process `PATHEXT`, so default behavior is unchanged. Returns `None`
165/// when unset everywhere, which is the normal case on non-Windows systems.
166fn env_path_ext(engine_state: &EngineState, stack: &Stack) -> Option<OsString> {
167    if let Some(value) = stack.get_env_var(engine_state, "pathext") {
168        return env::env_to_string("PATHEXT", value, engine_state, stack)
169            .ok()
170            .map(OsString::from);
171    }
172    if stack.is_env_var_hidden("PATHEXT") {
173        None
174    } else {
175        std::env::var_os("PATHEXT")
176    }
177}
178
179/// A [`which::sys::Sys`] that behaves like the real system in every respect
180/// except that `PATHEXT` is sourced from nushell's environment (`$env.PATHEXT`)
181/// instead of the process environment. This lets `which` honor in-shell changes
182/// to `PATHEXT` (for example via `with-env`), the same way it already honors
183/// in-shell changes to `PATH` (which is passed in explicitly).
184#[derive(Clone)]
185struct NuWhichSys {
186    path_ext: Option<OsString>,
187}
188
189impl Sys for NuWhichSys {
190    type ReadDirEntry = std::fs::DirEntry;
191    type Metadata = std::fs::Metadata;
192
193    fn is_windows(&self) -> bool {
194        RealSys.is_windows()
195    }
196
197    fn current_dir(&self) -> std::io::Result<PathBuf> {
198        RealSys.current_dir()
199    }
200
201    fn home_dir(&self) -> Option<PathBuf> {
202        RealSys.home_dir()
203    }
204
205    fn env_split_paths(&self, paths: &OsStr) -> Vec<PathBuf> {
206        RealSys.env_split_paths(paths)
207    }
208
209    fn env_path(&self) -> Option<OsString> {
210        RealSys.env_path()
211    }
212
213    fn env_path_ext(&self) -> Option<OsString> {
214        self.path_ext.clone()
215    }
216
217    // `env_windows_path_ext` is deliberately left as the trait default, which
218    // re-parses `self.env_path_ext()` on every call. `RealSys` overrides it to
219    // cache the process `PATHEXT` in a process-wide `OnceLock`, which would
220    // ignore `$env.PATHEXT`; the default keeps our value authoritative.
221
222    fn metadata(&self, path: &Path) -> std::io::Result<Self::Metadata> {
223        RealSys.metadata(path)
224    }
225
226    fn symlink_metadata(&self, path: &Path) -> std::io::Result<Self::Metadata> {
227        RealSys.symlink_metadata(path)
228    }
229
230    fn read_dir(
231        &self,
232        path: &Path,
233    ) -> std::io::Result<Box<dyn Iterator<Item = std::io::Result<Self::ReadDirEntry>>>> {
234        RealSys.read_dir(path)
235    }
236
237    fn is_valid_executable(&self, path: &Path) -> std::io::Result<bool> {
238        RealSys.is_valid_executable(path)
239    }
240}
241
242fn get_first_entry_in_path(
243    item: &str,
244    span: Span,
245    cwd: impl AsRef<Path>,
246    paths: impl AsRef<OsStr>,
247    path_ext: &Option<OsString>,
248) -> Option<Value> {
249    WhichConfig::new_with_sys(NuWhichSys {
250        path_ext: path_ext.clone(),
251    })
252    .binary_name(item.into())
253    .custom_cwd(cwd.as_ref().to_path_buf())
254    .custom_path_list(paths.as_ref().to_os_string())
255    .first_result()
256    .map(|path| {
257        let full_path = path.to_string_lossy().to_string();
258        entry(
259            item,
260            full_path.clone(),
261            CommandType::External,
262            None,
263            Some(full_path),
264            span,
265        )
266    })
267    .ok()
268}
269
270fn get_all_entries_in_path(
271    item: &str,
272    span: Span,
273    cwd: impl AsRef<Path>,
274    paths: impl AsRef<OsStr>,
275    path_ext: &Option<OsString>,
276) -> Vec<Value> {
277    // The results may contain the same canonical path more than once. On systems
278    // where PATH contains both a real directory and a symlink pointing to the same
279    // place (e.g. `/usr/bin` and `/bin -> /usr/bin` on WSL/Debian), the same path
280    // would appear multiple times. The HashSet deduplicates those before we build
281    // the output rows.
282    let mut seen = HashSet::new();
283    WhichConfig::new_with_sys(NuWhichSys {
284        path_ext: path_ext.clone(),
285    })
286    .binary_name(item.into())
287    .custom_cwd(cwd.as_ref().to_path_buf())
288    .custom_path_list(paths.as_ref().to_os_string())
289    .all_results()
290    .map(|iter| {
291        iter.filter(|path| seen.insert(path.clone()))
292            .map(|path| {
293                let full_path = path.to_string_lossy().to_string();
294                entry(
295                    item,
296                    full_path.clone(),
297                    CommandType::External,
298                    None,
299                    Some(full_path),
300                    span,
301                )
302            })
303            .collect()
304    })
305    .unwrap_or_default()
306}
307
308fn list_all_executables(
309    engine_state: &EngineState,
310    paths: impl AsRef<OsStr>,
311    path_ext: &Option<OsString>,
312    all: bool,
313    span: Span,
314) -> Vec<Value> {
315    let decls = engine_state.get_decls_sorted(false);
316
317    let mut results = Vec::with_capacity(decls.len());
318    let mut seen_commands = HashSet::with_capacity(decls.len());
319
320    for (name_bytes, decl_id) in decls {
321        let name = String::from_utf8_lossy(&name_bytes).to_string();
322        seen_commands.insert(name.clone());
323        let decl = engine_state.get_decl(decl_id);
324        let definition = if decl.command_type() == CommandType::Alias {
325            decl.as_alias().map(|alias| {
326                String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
327                    .to_string()
328            })
329        } else {
330            None
331        };
332        let file = file_for_decl(engine_state, decl);
333
334        results.push(entry(
335            name,
336            String::new(),
337            decl.command_type(),
338            definition,
339            file,
340            span,
341        ));
342    }
343
344    // Add PATH executables
345    let path_iter = RealSys
346        .env_split_paths(paths.as_ref())
347        .into_iter()
348        .filter_map(|dir| fs::read_dir(dir).ok())
349        .flat_map(|entries| entries.flatten())
350        .map(|entry| entry.path())
351        .filter_map(|path| {
352            if !path.is_executable(path_ext.as_deref()) {
353                return None;
354            }
355            let filename = path.file_name()?.to_string_lossy().to_string();
356
357            if !all && !seen_commands.insert(filename.clone()) {
358                return None;
359            }
360
361            let full_path = path.to_string_lossy().to_string();
362            Some(entry(
363                filename,
364                full_path.clone(),
365                CommandType::External,
366                None,
367                Some(full_path),
368                span,
369            ))
370        });
371
372    results.extend(path_iter);
373    results
374}
375
376#[derive(Debug)]
377struct WhichArgs {
378    applications: Vec<Spanned<String>>,
379    all: bool,
380}
381
382fn which_single(
383    application: Spanned<String>,
384    all: bool,
385    engine_state: &EngineState,
386    cwd: impl AsRef<Path>,
387    paths: impl AsRef<OsStr>,
388    path_ext: &Option<OsString>,
389) -> Vec<Value> {
390    let cwd = cwd.as_ref();
391    let paths = paths.as_ref();
392    let (external, prog_name) = if application.item.starts_with('^') {
393        (true, application.item[1..].to_string())
394    } else {
395        (false, application.item.clone())
396    };
397
398    // If prog_name is an external command, don't search for nu-specific programs.
399    // If all is false, we can save some time by only searching for the first match.
400    match (all, external) {
401        (true, true) => get_all_entries_in_path(&prog_name, application.span, cwd, paths, path_ext),
402        (true, false) => {
403            let mut output: Vec<Value> = vec![];
404            if let Some(entry) = get_entry_in_commands(engine_state, &prog_name, application.span) {
405                output.push(entry);
406            }
407            output.extend(get_all_entries_in_path(
408                &prog_name,
409                application.span,
410                cwd,
411                paths,
412                path_ext,
413            ));
414            output
415        }
416        (false, true) => {
417            get_first_entry_in_path(&prog_name, application.span, cwd, paths, path_ext)
418                .into_iter()
419                .collect()
420        }
421        (false, false) => get_entry_in_commands(engine_state, &prog_name, application.span)
422            .or_else(|| get_first_entry_in_path(&prog_name, application.span, cwd, paths, path_ext))
423            .into_iter()
424            .collect(),
425    }
426}
427
428fn which(
429    engine_state: &EngineState,
430    stack: &mut Stack,
431    call: &Call,
432) -> Result<PipelineData, ShellError> {
433    let head = call.head;
434    let which_args = WhichArgs {
435        applications: call.rest(engine_state, stack, 0)?,
436        all: call.has_flag(engine_state, stack, "all")?,
437    };
438
439    let mut output = vec![];
440
441    let cwd = engine_state.cwd_as_string(Some(stack))?;
442
443    // PATH may not be set in minimal environments (e.g. plugin test harnesses).
444    // In that case we can still resolve built-ins, aliases, custom commands and
445    // known externals; we just won't find any PATH-based binaries.
446    let paths = env::path_str(engine_state, stack, head).unwrap_or_default();
447
448    // Source PATHEXT from the shell environment so `which` honors in-shell
449    // changes to `$env.PATHEXT`, just like it already honors `$env.PATH`.
450    let path_ext = env_path_ext(engine_state, stack);
451
452    let metadata = PipelineMetadata::default().with_path_columns(vec!["path".into()]);
453
454    if which_args.applications.is_empty() {
455        return Ok(
456            list_all_executables(engine_state, &paths, &path_ext, which_args.all, head)
457                .into_iter()
458                .into_pipeline_data(head, engine_state.signals().clone())
459                .set_metadata(Some(metadata)),
460        );
461    }
462
463    for app in which_args.applications {
464        let values = which_single(app, which_args.all, engine_state, &cwd, &paths, &path_ext);
465        output.extend(values);
466    }
467
468    Ok(output
469        .into_iter()
470        .into_pipeline_data(head, engine_state.signals().clone())
471        .set_metadata(Some(metadata)))
472}
473
474#[cfg(test)]
475mod test {
476    use super::*;
477
478    #[test]
479    fn test_examples() -> nu_test_support::Result {
480        nu_test_support::test().examples(Which)
481    }
482}
483
484// --------------------
485// Copied from https://docs.rs/is_executable/ v1.0.5
486// Removed path.exists() check in `mod windows`.
487
488/// An extension trait for `std::fs::Path` providing an `is_executable` method.
489///
490/// See the module documentation for examples.
491pub trait IsExecutable {
492    /// Returns `true` if there is a file at the given path and it is
493    /// executable. Returns `false` otherwise.
494    ///
495    /// On Windows, `path_ext` is the shell's `PATHEXT` (`$env.PATHEXT`) used to
496    /// decide whether a file extension counts as executable; passing `None`
497    /// falls back to checking the binary type. On other platforms it is ignored.
498    ///
499    /// See the module documentation for details.
500    fn is_executable(&self, path_ext: Option<&OsStr>) -> bool;
501}
502
503#[cfg(unix)]
504mod unix {
505    use std::os::unix::fs::PermissionsExt;
506    use std::path::Path;
507
508    use super::IsExecutable;
509
510    impl IsExecutable for Path {
511        fn is_executable(&self, _path_ext: Option<&std::ffi::OsStr>) -> bool {
512            let metadata = match self.metadata() {
513                Ok(metadata) => metadata,
514                Err(_) => return false,
515            };
516            let permissions = metadata.permissions();
517            metadata.is_file() && permissions.mode() & 0o111 != 0
518        }
519    }
520}
521
522#[cfg(target_os = "windows")]
523mod windows {
524    use std::os::windows::ffi::OsStrExt;
525    use std::path::Path;
526
527    use windows::Win32::Storage::FileSystem::GetBinaryTypeW;
528    use windows::core::PCWSTR;
529
530    use super::IsExecutable;
531
532    impl IsExecutable for Path {
533        fn is_executable(&self, path_ext: Option<&std::ffi::OsStr>) -> bool {
534            // Check using file extension against the shell's `$env.PATHEXT`.
535            if let Some(pathext) = path_ext
536                && let Some(extension) = self.extension()
537            {
538                let extension = extension.to_string_lossy();
539
540                // Originally taken from:
541                // https://github.com/nushell/nushell/blob/93e8f6c05e1e1187d5b674d6b633deb839c84899/crates/nu-cli/src/completion/command.rs#L64-L74
542                return pathext
543                    .to_string_lossy()
544                    .split(';')
545                    // Filter out empty tokens and ';' at the end
546                    .filter(|f| f.len() > 1)
547                    .any(|ext| {
548                        // Cut off the leading '.' character
549                        let ext = &ext[1..];
550                        extension.eq_ignore_ascii_case(ext)
551                    });
552            }
553
554            // Check using file properties
555            // This code is only reached if there is no file extension or retrieving PATHEXT fails
556            let windows_string: Vec<u16> = self.as_os_str().encode_wide().chain(Some(0)).collect();
557            let mut binary_type: u32 = 0;
558
559            let result =
560                unsafe { GetBinaryTypeW(PCWSTR(windows_string.as_ptr()), &mut binary_type) };
561            if result.is_ok()
562                && let 0..=6 = binary_type
563            {
564                return true;
565            }
566
567            false
568        }
569    }
570}
571
572// For WASI, we can't check if a file is executable
573// Since wasm and wasi
574//  is not supposed to add executables ideologically,
575// specify them collectively
576#[cfg(any(target_os = "wasi", target_family = "wasm"))]
577mod wasm {
578    use std::path::Path;
579
580    use super::IsExecutable;
581
582    impl IsExecutable for Path {
583        fn is_executable(&self, _path_ext: Option<&std::ffi::OsStr>) -> bool {
584            false
585        }
586    }
587}