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// 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    definition: Option<String>,
73    span: Span,
74) -> Value {
75    let mut record = record! {
76        "command" => Value::string(arg, span),
77        "path" => Value::string(path, span),
78        "type" => Value::string(cmd_type.to_string(), span),
79    };
80
81    if let Some(def) = definition {
82        record.insert("definition", Value::string(def, span));
83    }
84
85    Value::record(record, span)
86}
87
88fn get_entry_in_commands(engine_state: &EngineState, name: &str, span: Span) -> Option<Value> {
89    if let Some(decl_id) = engine_state.find_decl(name.as_bytes(), &[]) {
90        let decl = engine_state.get_decl(decl_id);
91        let definition = if decl.command_type() == CommandType::Alias {
92            decl.as_alias().map(|alias| {
93                String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
94                    .to_string()
95            })
96        } else {
97            None
98        };
99        Some(entry(name, "", decl.command_type(), definition, span))
100    } else {
101        None
102    }
103}
104
105fn get_first_entry_in_path(
106    item: &str,
107    span: Span,
108    cwd: impl AsRef<Path>,
109    paths: impl AsRef<OsStr>,
110) -> Option<Value> {
111    which::which_in(item, Some(paths), cwd)
112        .map(|path| {
113            entry(
114                item,
115                path.to_string_lossy(),
116                CommandType::External,
117                None,
118                span,
119            )
120        })
121        .ok()
122}
123
124fn get_all_entries_in_path(
125    item: &str,
126    span: Span,
127    cwd: impl AsRef<Path>,
128    paths: impl AsRef<OsStr>,
129) -> Vec<Value> {
130    which::which_in_all(&item, Some(paths), cwd)
131        .map(|iter| {
132            iter.map(|path| {
133                entry(
134                    item,
135                    path.to_string_lossy(),
136                    CommandType::External,
137                    None,
138                    span,
139                )
140            })
141            .collect()
142        })
143        .unwrap_or_default()
144}
145
146fn list_all_executables(
147    engine_state: &EngineState,
148    paths: impl AsRef<OsStr>,
149    all: bool,
150) -> Vec<Value> {
151    let decls = engine_state.get_decls_sorted(false);
152
153    let mut results = Vec::with_capacity(decls.len());
154    let mut seen_commands = HashSet::with_capacity(decls.len());
155
156    for (name_bytes, decl_id) in decls {
157        let name = String::from_utf8_lossy(&name_bytes).to_string();
158        seen_commands.insert(name.clone());
159        let decl = engine_state.get_decl(decl_id);
160        let definition = if decl.command_type() == CommandType::Alias {
161            decl.as_alias().map(|alias| {
162                String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
163                    .to_string()
164            })
165        } else {
166            None
167        };
168        results.push(entry(
169            name,
170            String::new(),
171            decl.command_type(),
172            definition,
173            Span::unknown(),
174        ));
175    }
176
177    // Add PATH executables
178    let path_iter = sys::RealSys
179        .env_split_paths(paths.as_ref())
180        .into_iter()
181        .filter_map(|dir| fs::read_dir(dir).ok())
182        .flat_map(|entries| entries.flatten())
183        .map(|entry| entry.path())
184        .filter_map(|path| {
185            if !path.is_executable() {
186                return None;
187            }
188            let filename = path.file_name()?.to_string_lossy().to_string();
189
190            if !all && !seen_commands.insert(filename.clone()) {
191                return None;
192            }
193
194            let full_path = path.to_string_lossy().to_string();
195            Some(entry(
196                filename,
197                full_path,
198                CommandType::External,
199                None,
200                Span::unknown(),
201            ))
202        });
203
204    results.extend(path_iter);
205    results
206}
207
208#[derive(Debug)]
209struct WhichArgs {
210    applications: Vec<Spanned<String>>,
211    all: bool,
212}
213
214fn which_single(
215    application: Spanned<String>,
216    all: bool,
217    engine_state: &EngineState,
218    cwd: impl AsRef<Path>,
219    paths: impl AsRef<OsStr>,
220) -> Vec<Value> {
221    let (external, prog_name) = if application.item.starts_with('^') {
222        (true, application.item[1..].to_string())
223    } else {
224        (false, application.item.clone())
225    };
226
227    //If prog_name is an external command, don't search for nu-specific programs
228    //If all is false, we can save some time by only searching for the first matching
229    //program
230    //This match handles all different cases
231    match (all, external) {
232        (true, true) => get_all_entries_in_path(&prog_name, application.span, cwd, paths),
233        (true, false) => {
234            let mut output: Vec<Value> = vec![];
235            if let Some(entry) = get_entry_in_commands(engine_state, &prog_name, application.span) {
236                output.push(entry);
237            }
238            output.extend(get_all_entries_in_path(
239                &prog_name,
240                application.span,
241                cwd,
242                paths,
243            ));
244            output
245        }
246        (false, true) => get_first_entry_in_path(&prog_name, application.span, cwd, paths)
247            .into_iter()
248            .collect(),
249        (false, false) => get_entry_in_commands(engine_state, &prog_name, application.span)
250            .or_else(|| get_first_entry_in_path(&prog_name, application.span, cwd, paths))
251            .into_iter()
252            .collect(),
253    }
254}
255
256fn which(
257    engine_state: &EngineState,
258    stack: &mut Stack,
259    call: &Call,
260) -> Result<PipelineData, ShellError> {
261    let head = call.head;
262    let which_args = WhichArgs {
263        applications: call.rest(engine_state, stack, 0)?,
264        all: call.has_flag(engine_state, stack, "all")?,
265    };
266
267    let mut output = vec![];
268
269    let cwd = engine_state.cwd_as_string(Some(stack))?;
270    let paths = env::path_str(engine_state, stack, head)?;
271
272    if which_args.applications.is_empty() {
273        return Ok(list_all_executables(engine_state, paths, which_args.all)
274            .into_iter()
275            .into_pipeline_data(head, engine_state.signals().clone()));
276    }
277
278    for app in which_args.applications {
279        let values = which_single(app, which_args.all, engine_state, &cwd, &paths);
280        output.extend(values);
281    }
282
283    Ok(output
284        .into_iter()
285        .into_pipeline_data(head, engine_state.signals().clone()))
286}
287
288#[cfg(test)]
289mod test {
290    use super::*;
291
292    #[test]
293    fn test_examples() {
294        crate::test_examples(Which)
295    }
296}
297
298// --------------------
299// Copied from https://docs.rs/is_executable/ v1.0.5
300// Removed path.exists() check in `mod windows`.
301
302/// An extension trait for `std::fs::Path` providing an `is_executable` method.
303///
304/// See the module documentation for examples.
305pub trait IsExecutable {
306    /// Returns `true` if there is a file at the given path and it is
307    /// executable. Returns `false` otherwise.
308    ///
309    /// See the module documentation for details.
310    fn is_executable(&self) -> bool;
311}
312
313#[cfg(unix)]
314mod unix {
315    use std::os::unix::fs::PermissionsExt;
316    use std::path::Path;
317
318    use super::IsExecutable;
319
320    impl IsExecutable for Path {
321        fn is_executable(&self) -> bool {
322            let metadata = match self.metadata() {
323                Ok(metadata) => metadata,
324                Err(_) => return false,
325            };
326            let permissions = metadata.permissions();
327            metadata.is_file() && permissions.mode() & 0o111 != 0
328        }
329    }
330}
331
332#[cfg(target_os = "windows")]
333mod windows {
334    use std::os::windows::ffi::OsStrExt;
335    use std::path::Path;
336
337    use windows::Win32::Storage::FileSystem::GetBinaryTypeW;
338    use windows::core::PCWSTR;
339
340    use super::IsExecutable;
341
342    impl IsExecutable for Path {
343        fn is_executable(&self) -> bool {
344            // Check using file extension
345            if let Some(pathext) = std::env::var_os("PATHEXT")
346                && let Some(extension) = self.extension()
347            {
348                let extension = extension.to_string_lossy();
349
350                // Originally taken from:
351                // https://github.com/nushell/nushell/blob/93e8f6c05e1e1187d5b674d6b633deb839c84899/crates/nu-cli/src/completion/command.rs#L64-L74
352                return pathext
353                    .to_string_lossy()
354                    .split(';')
355                    // Filter out empty tokens and ';' at the end
356                    .filter(|f| f.len() > 1)
357                    .any(|ext| {
358                        // Cut off the leading '.' character
359                        let ext = &ext[1..];
360                        extension.eq_ignore_ascii_case(ext)
361                    });
362            }
363
364            // Check using file properties
365            // This code is only reached if there is no file extension or retrieving PATHEXT fails
366            let windows_string: Vec<u16> = self.as_os_str().encode_wide().chain(Some(0)).collect();
367            let mut binary_type: u32 = 0;
368
369            let result =
370                unsafe { GetBinaryTypeW(PCWSTR(windows_string.as_ptr()), &mut binary_type) };
371            if result.is_ok()
372                && let 0..=6 = binary_type
373            {
374                return true;
375            }
376
377            false
378        }
379    }
380}
381
382// For WASI, we can't check if a file is executable
383// Since wasm and wasi
384//  is not supposed to add executables ideologically,
385// specify them collectively
386#[cfg(any(target_os = "wasi", target_family = "wasm"))]
387mod wasm {
388    use std::path::Path;
389
390    use super::IsExecutable;
391
392    impl IsExecutable for Path {
393        fn is_executable(&self) -> bool {
394            false
395        }
396    }
397}