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", "get-command", ]
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
67fn 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
75fn 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
103fn 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 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) -> Vec<Value> {
205 let decls = engine_state.get_decls_sorted(false);
206
207 let mut results = Vec::with_capacity(decls.len());
208 let mut seen_commands = HashSet::with_capacity(decls.len());
209
210 for (name_bytes, decl_id) in decls {
211 let name = String::from_utf8_lossy(&name_bytes).to_string();
212 seen_commands.insert(name.clone());
213 let decl = engine_state.get_decl(decl_id);
214 let definition = if decl.command_type() == CommandType::Alias {
215 decl.as_alias().map(|alias| {
216 String::from_utf8_lossy(engine_state.get_span_contents(alias.wrapped_call.span))
217 .to_string()
218 })
219 } else {
220 None
221 };
222 let file = file_for_decl(engine_state, decl);
223
224 results.push(entry(
225 name,
226 String::new(),
227 decl.command_type(),
228 definition,
229 file,
230 Span::unknown(),
231 ));
232 }
233
234 let path_iter = sys::RealSys
236 .env_split_paths(paths.as_ref())
237 .into_iter()
238 .filter_map(|dir| fs::read_dir(dir).ok())
239 .flat_map(|entries| entries.flatten())
240 .map(|entry| entry.path())
241 .filter_map(|path| {
242 if !path.is_executable() {
243 return None;
244 }
245 let filename = path.file_name()?.to_string_lossy().to_string();
246
247 if !all && !seen_commands.insert(filename.clone()) {
248 return None;
249 }
250
251 let full_path = path.to_string_lossy().to_string();
252 Some(entry(
253 filename,
254 full_path.clone(),
255 CommandType::External,
256 None,
257 Some(full_path),
258 Span::unknown(),
259 ))
260 });
261
262 results.extend(path_iter);
263 results
264}
265
266#[derive(Debug)]
267struct WhichArgs {
268 applications: Vec<Spanned<String>>,
269 all: bool,
270}
271
272fn which_single(
273 application: Spanned<String>,
274 all: bool,
275 engine_state: &EngineState,
276 cwd: impl AsRef<Path>,
277 paths: impl AsRef<OsStr>,
278) -> Vec<Value> {
279 let cwd = cwd.as_ref();
280 let paths = paths.as_ref();
281 let (external, prog_name) = if application.item.starts_with('^') {
282 (true, application.item[1..].to_string())
283 } else {
284 (false, application.item.clone())
285 };
286
287 match (all, external) {
290 (true, true) => get_all_entries_in_path(&prog_name, application.span, cwd, paths),
291 (true, false) => {
292 let mut output: Vec<Value> = vec![];
293 if let Some(entry) = get_entry_in_commands(engine_state, &prog_name, application.span) {
294 output.push(entry);
295 }
296 output.extend(get_all_entries_in_path(
297 &prog_name,
298 application.span,
299 cwd,
300 paths,
301 ));
302 output
303 }
304 (false, true) => get_first_entry_in_path(&prog_name, application.span, cwd, paths)
305 .into_iter()
306 .collect(),
307 (false, false) => get_entry_in_commands(engine_state, &prog_name, application.span)
308 .or_else(|| get_first_entry_in_path(&prog_name, application.span, cwd, paths))
309 .into_iter()
310 .collect(),
311 }
312}
313
314fn which(
315 engine_state: &EngineState,
316 stack: &mut Stack,
317 call: &Call,
318) -> Result<PipelineData, ShellError> {
319 let head = call.head;
320 let which_args = WhichArgs {
321 applications: call.rest(engine_state, stack, 0)?,
322 all: call.has_flag(engine_state, stack, "all")?,
323 };
324
325 let mut output = vec![];
326
327 let cwd = engine_state.cwd_as_string(Some(stack))?;
328
329 let paths = env::path_str(engine_state, stack, head).unwrap_or_default();
333
334 if which_args.applications.is_empty() {
335 return Ok(list_all_executables(engine_state, &paths, which_args.all)
336 .into_iter()
337 .into_pipeline_data(head, engine_state.signals().clone()));
338 }
339
340 for app in which_args.applications {
341 let values = which_single(app, which_args.all, engine_state, &cwd, &paths);
342 output.extend(values);
343 }
344
345 Ok(output
346 .into_iter()
347 .into_pipeline_data(head, engine_state.signals().clone()))
348}
349
350#[cfg(test)]
351mod test {
352 use super::*;
353
354 #[test]
355 fn test_examples() {
356 crate::test_examples(Which)
357 }
358}
359
360pub trait IsExecutable {
368 fn is_executable(&self) -> bool;
373}
374
375#[cfg(unix)]
376mod unix {
377 use std::os::unix::fs::PermissionsExt;
378 use std::path::Path;
379
380 use super::IsExecutable;
381
382 impl IsExecutable for Path {
383 fn is_executable(&self) -> bool {
384 let metadata = match self.metadata() {
385 Ok(metadata) => metadata,
386 Err(_) => return false,
387 };
388 let permissions = metadata.permissions();
389 metadata.is_file() && permissions.mode() & 0o111 != 0
390 }
391 }
392}
393
394#[cfg(target_os = "windows")]
395mod windows {
396 use std::os::windows::ffi::OsStrExt;
397 use std::path::Path;
398
399 use windows::Win32::Storage::FileSystem::GetBinaryTypeW;
400 use windows::core::PCWSTR;
401
402 use super::IsExecutable;
403
404 impl IsExecutable for Path {
405 fn is_executable(&self) -> bool {
406 if let Some(pathext) = std::env::var_os("PATHEXT")
408 && let Some(extension) = self.extension()
409 {
410 let extension = extension.to_string_lossy();
411
412 return pathext
415 .to_string_lossy()
416 .split(';')
417 .filter(|f| f.len() > 1)
419 .any(|ext| {
420 let ext = &ext[1..];
422 extension.eq_ignore_ascii_case(ext)
423 });
424 }
425
426 let windows_string: Vec<u16> = self.as_os_str().encode_wide().chain(Some(0)).collect();
429 let mut binary_type: u32 = 0;
430
431 let result =
432 unsafe { GetBinaryTypeW(PCWSTR(windows_string.as_ptr()), &mut binary_type) };
433 if result.is_ok()
434 && let 0..=6 = binary_type
435 {
436 return true;
437 }
438
439 false
440 }
441 }
442}
443
444#[cfg(any(target_os = "wasi", target_family = "wasm"))]
449mod wasm {
450 use std::path::Path;
451
452 use super::IsExecutable;
453
454 impl IsExecutable for Path {
455 fn is_executable(&self) -> bool {
456 false
457 }
458 }
459}