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", "get-command", ]
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
68fn 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
76fn 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
104fn 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 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 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 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 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
368pub trait IsExecutable {
376 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 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 return pathext
423 .to_string_lossy()
424 .split(';')
425 .filter(|f| f.len() > 1)
427 .any(|ext| {
428 let ext = &ext[1..];
430 extension.eq_ignore_ascii_case(ext)
431 });
432 }
433
434 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#[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}