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    span: Span,
205) -> Vec<Value> {
206    let decls = engine_state.get_decls_sorted(false);
207
208    let mut results = Vec::with_capacity(decls.len());
209    let mut seen_commands = HashSet::with_capacity(decls.len());
210
211    for (name_bytes, decl_id) in decls {
212        let name = String::from_utf8_lossy(&name_bytes).to_string();
213        seen_commands.insert(name.clone());
214        let decl = engine_state.get_decl(decl_id);
215        let definition = if decl.command_type() == CommandType::Alias {
216            decl.as_alias().map(|alias| {
217                String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
218                    .to_string()
219            })
220        } else {
221            None
222        };
223        let file = file_for_decl(engine_state, decl);
224
225        results.push(entry(
226            name,
227            String::new(),
228            decl.command_type(),
229            definition,
230            file,
231            span,
232        ));
233    }
234
235    // Add PATH executables
236    let path_iter = sys::RealSys
237        .env_split_paths(paths.as_ref())
238        .into_iter()
239        .filter_map(|dir| fs::read_dir(dir).ok())
240        .flat_map(|entries| entries.flatten())
241        .map(|entry| entry.path())
242        .filter_map(|path| {
243            if !path.is_executable() {
244                return None;
245            }
246            let filename = path.file_name()?.to_string_lossy().to_string();
247
248            if !all && !seen_commands.insert(filename.clone()) {
249                return None;
250            }
251
252            let full_path = path.to_string_lossy().to_string();
253            Some(entry(
254                filename,
255                full_path.clone(),
256                CommandType::External,
257                None,
258                Some(full_path),
259                span,
260            ))
261        });
262
263    results.extend(path_iter);
264    results
265}
266
267#[derive(Debug)]
268struct WhichArgs {
269    applications: Vec<Spanned<String>>,
270    all: bool,
271}
272
273fn which_single(
274    application: Spanned<String>,
275    all: bool,
276    engine_state: &EngineState,
277    cwd: impl AsRef<Path>,
278    paths: impl AsRef<OsStr>,
279) -> Vec<Value> {
280    let cwd = cwd.as_ref();
281    let paths = paths.as_ref();
282    let (external, prog_name) = if application.item.starts_with('^') {
283        (true, application.item[1..].to_string())
284    } else {
285        (false, application.item.clone())
286    };
287
288    // If prog_name is an external command, don't search for nu-specific programs.
289    // If all is false, we can save some time by only searching for the first match.
290    match (all, external) {
291        (true, true) => get_all_entries_in_path(&prog_name, application.span, cwd, paths),
292        (true, false) => {
293            let mut output: Vec<Value> = vec![];
294            if let Some(entry) = get_entry_in_commands(engine_state, &prog_name, application.span) {
295                output.push(entry);
296            }
297            output.extend(get_all_entries_in_path(
298                &prog_name,
299                application.span,
300                cwd,
301                paths,
302            ));
303            output
304        }
305        (false, true) => get_first_entry_in_path(&prog_name, application.span, cwd, paths)
306            .into_iter()
307            .collect(),
308        (false, false) => get_entry_in_commands(engine_state, &prog_name, application.span)
309            .or_else(|| get_first_entry_in_path(&prog_name, application.span, cwd, paths))
310            .into_iter()
311            .collect(),
312    }
313}
314
315fn which(
316    engine_state: &EngineState,
317    stack: &mut Stack,
318    call: &Call,
319) -> Result<PipelineData, ShellError> {
320    let head = call.head;
321    let which_args = WhichArgs {
322        applications: call.rest(engine_state, stack, 0)?,
323        all: call.has_flag(engine_state, stack, "all")?,
324    };
325
326    let mut output = vec![];
327
328    let cwd = engine_state.cwd_as_string(Some(stack))?;
329
330    // PATH may not be set in minimal environments (e.g. plugin test harnesses).
331    // In that case we can still resolve built-ins, aliases, custom commands and
332    // known externals; we just won't find any PATH-based binaries.
333    let paths = env::path_str(engine_state, stack, head).unwrap_or_default();
334
335    if which_args.applications.is_empty() {
336        return Ok(
337            list_all_executables(engine_state, &paths, which_args.all, head)
338                .into_iter()
339                .into_pipeline_data(head, engine_state.signals().clone()),
340        );
341    }
342
343    for app in which_args.applications {
344        let values = which_single(app, which_args.all, engine_state, &cwd, &paths);
345        output.extend(values);
346    }
347
348    Ok(output
349        .into_iter()
350        .into_pipeline_data(head, engine_state.signals().clone()))
351}
352
353#[cfg(test)]
354mod test {
355    use super::*;
356
357    #[test]
358    fn test_examples() -> nu_test_support::Result {
359        nu_test_support::test().examples(Which)
360    }
361}
362
363// --------------------
364// Copied from https://docs.rs/is_executable/ v1.0.5
365// Removed path.exists() check in `mod windows`.
366
367/// An extension trait for `std::fs::Path` providing an `is_executable` method.
368///
369/// See the module documentation for examples.
370pub trait IsExecutable {
371    /// Returns `true` if there is a file at the given path and it is
372    /// executable. Returns `false` otherwise.
373    ///
374    /// See the module documentation for details.
375    fn is_executable(&self) -> bool;
376}
377
378#[cfg(unix)]
379mod unix {
380    use std::os::unix::fs::PermissionsExt;
381    use std::path::Path;
382
383    use super::IsExecutable;
384
385    impl IsExecutable for Path {
386        fn is_executable(&self) -> bool {
387            let metadata = match self.metadata() {
388                Ok(metadata) => metadata,
389                Err(_) => return false,
390            };
391            let permissions = metadata.permissions();
392            metadata.is_file() && permissions.mode() & 0o111 != 0
393        }
394    }
395}
396
397#[cfg(target_os = "windows")]
398mod windows {
399    use std::os::windows::ffi::OsStrExt;
400    use std::path::Path;
401
402    use windows::Win32::Storage::FileSystem::GetBinaryTypeW;
403    use windows::core::PCWSTR;
404
405    use super::IsExecutable;
406
407    impl IsExecutable for Path {
408        fn is_executable(&self) -> bool {
409            // Check using file extension
410            if let Some(pathext) = std::env::var_os("PATHEXT")
411                && let Some(extension) = self.extension()
412            {
413                let extension = extension.to_string_lossy();
414
415                // Originally taken from:
416                // https://github.com/nushell/nushell/blob/93e8f6c05e1e1187d5b674d6b633deb839c84899/crates/nu-cli/src/completion/command.rs#L64-L74
417                return pathext
418                    .to_string_lossy()
419                    .split(';')
420                    // Filter out empty tokens and ';' at the end
421                    .filter(|f| f.len() > 1)
422                    .any(|ext| {
423                        // Cut off the leading '.' character
424                        let ext = &ext[1..];
425                        extension.eq_ignore_ascii_case(ext)
426                    });
427            }
428
429            // Check using file properties
430            // This code is only reached if there is no file extension or retrieving PATHEXT fails
431            let windows_string: Vec<u16> = self.as_os_str().encode_wide().chain(Some(0)).collect();
432            let mut binary_type: u32 = 0;
433
434            let result =
435                unsafe { GetBinaryTypeW(PCWSTR(windows_string.as_ptr()), &mut binary_type) };
436            if result.is_ok()
437                && let 0..=6 = binary_type
438            {
439                return true;
440            }
441
442            false
443        }
444    }
445}
446
447// For WASI, we can't check if a file is executable
448// Since wasm and wasi
449//  is not supposed to add executables ideologically,
450// specify them collectively
451#[cfg(any(target_os = "wasi", target_family = "wasm"))]
452mod wasm {
453    use std::path::Path;
454
455    use super::IsExecutable;
456
457    impl IsExecutable for Path {
458        fn is_executable(&self) -> bool {
459            false
460        }
461    }
462}