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", "get-command", ]
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
75fn 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
83fn 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
111fn 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
156fn 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#[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 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 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 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 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 let paths = env::path_str(engine_state, stack, head).unwrap_or_default();
447
448 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
484pub trait IsExecutable {
492 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 if let Some(pathext) = path_ext
536 && let Some(extension) = self.extension()
537 {
538 let extension = extension.to_string_lossy();
539
540 return pathext
543 .to_string_lossy()
544 .split(';')
545 .filter(|f| f.len() > 1)
547 .any(|ext| {
548 let ext = &ext[1..];
550 extension.eq_ignore_ascii_case(ext)
551 });
552 }
553
554 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#[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}