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