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 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 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 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
298pub trait IsExecutable {
306 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 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 return pathext
353 .to_string_lossy()
354 .split(';')
355 .filter(|f| f.len() > 1)
357 .any(|ext| {
358 let ext = &ext[1..];
360 extension.eq_ignore_ascii_case(ext)
361 });
362 }
363
364 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#[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}