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