1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::{CommandFactory, FromArgMatches};
6use std::{
7 collections::BTreeSet,
8 fs,
9 os::unix::fs::PermissionsExt,
10 path::{Path, PathBuf},
11 process::ExitCode,
12};
13
14#[derive(Debug, Default)]
15pub struct Args {
16 pub search_binaries: bool,
17 pub search_manuals: bool,
18 pub search_sources: bool,
19 pub unusual_only: bool,
20 pub list_paths: bool,
21 pub glob_mode: bool,
22 pub custom_bin_dirs: Vec<PathBuf>,
23 pub custom_man_dirs: Vec<PathBuf>,
24 pub custom_src_dirs: Vec<PathBuf>,
25 pub names: Vec<String>,
26}
27
28#[derive(clap::Parser)]
29#[command(
30 name = "whereis",
31 about = "Locate the binary, source, and manual page files for a command"
32)]
33struct ClapArgs {
34 #[arg(short = 'b', help = "Search for binaries only")]
35 binaries: bool,
36 #[arg(short = 'm', help = "Search for manuals only")]
37 manuals: bool,
38 #[arg(short = 's', help = "Search for sources only")]
39 sources: bool,
40 #[arg(short = 'u', help = "Only show unusual entries")]
41 unusual: bool,
42 #[arg(short = 'l', help = "List effective lookup paths")]
43 list: bool,
44 #[arg(short = 'g', help = "Interpret names as glob patterns")]
45 glob: bool,
46}
47
48impl Args {
49 pub fn parse_from(raw: &[String]) -> Self {
50 let mut args = Args::default();
51
52 let mut i = 0;
53 while i < raw.len() {
54 match raw[i].as_str() {
55 "-b" => args.search_binaries = true,
56 "-m" => args.search_manuals = true,
57 "-s" => args.search_sources = true,
58 "-u" => args.unusual_only = true,
59 "-l" => args.list_paths = true,
60 "-g" => args.glob_mode = true,
61 "-B" => {
62 i += 1;
63 while i < raw.len() && raw[i] != "-f" {
64 args.custom_bin_dirs.push(PathBuf::from(&raw[i]));
65 i += 1;
66 }
67 if i >= raw.len() || raw[i] != "-f" {
68 eprintln!(
69 "whereis: -B requires -f to terminate the directory list"
70 );
71 std::process::exit(1);
72 }
73 }
74 "-M" => {
75 i += 1;
76 while i < raw.len() && raw[i] != "-f" {
77 args.custom_man_dirs.push(PathBuf::from(&raw[i]));
78 i += 1;
79 }
80 if i >= raw.len() || raw[i] != "-f" {
81 eprintln!(
82 "whereis: -M requires -f to terminate the directory list"
83 );
84 std::process::exit(1);
85 }
86 }
87 "-S" => {
88 i += 1;
89 while i < raw.len() && raw[i] != "-f" {
90 args.custom_src_dirs.push(PathBuf::from(&raw[i]));
91 i += 1;
92 }
93 if i >= raw.len() || raw[i] != "-f" {
94 eprintln!(
95 "whereis: -S requires -f to terminate the directory list"
96 );
97 std::process::exit(1);
98 }
99 }
100 "-h" | "--help" => {
101 let _ = ClapArgs::command().print_help();
102 println!();
103 std::process::exit(0);
104 }
105 "-V" | "--version" => {
106 let cmd = ClapArgs::command();
107 println!(
108 "{} {}",
109 cmd.get_name(),
110 cmd.get_version().unwrap_or("")
111 );
112 std::process::exit(0);
113 }
114 other if other.starts_with('-') && other.len() > 1 => {
115 let chars: Vec<char> = other[1..].chars().collect();
116 let mut j = 0;
117 while j < chars.len() {
118 match chars[j] {
119 'b' => args.search_binaries = true,
120 'm' => args.search_manuals = true,
121 's' => args.search_sources = true,
122 'u' => args.unusual_only = true,
123 'l' => args.list_paths = true,
124 'g' => args.glob_mode = true,
125 c => {
126 eprintln!("whereis: invalid option -- '{c}'");
127 std::process::exit(1);
128 }
129 }
130 j += 1;
131 }
132 }
133 name => {
134 args.names.push(name.to_string());
135 }
136 }
137 i += 1;
138 }
139
140 args
141 }
142
143 pub fn command() -> clap::Command {
144 ClapArgs::command()
145 }
146
147 pub fn from_arg_matches(m: &clap::ArgMatches) -> Result<Self, clap::Error> {
148 let clap_args = ClapArgs::from_arg_matches(m)?;
149 Ok(Args {
150 search_binaries: clap_args.binaries,
151 search_manuals: clap_args.manuals,
152 search_sources: clap_args.sources,
153 unusual_only: clap_args.unusual,
154 list_paths: clap_args.list,
155 glob_mode: clap_args.glob,
156 custom_bin_dirs: Vec::new(),
157 custom_man_dirs: Vec::new(),
158 custom_src_dirs: Vec::new(),
159 names: Vec::new(),
160 })
161 }
162}
163
164fn default_bin_dirs() -> Vec<PathBuf> {
165 let mut dirs: Vec<PathBuf> = [
166 "/usr/bin",
167 "/usr/sbin",
168 "/bin",
169 "/sbin",
170 "/usr/local/bin",
171 "/usr/local/sbin",
172 "/usr/games",
173 "/usr/local/games",
174 ]
175 .iter()
176 .map(PathBuf::from)
177 .collect();
178
179 if let Ok(path) = std::env::var("PATH") {
180 for p in path.split(':') {
181 if !p.is_empty() {
182 let pb = PathBuf::from(p);
183 if !dirs.contains(&pb) {
184 dirs.push(pb);
185 }
186 }
187 }
188 }
189
190 dirs.into_iter().filter(|d| d.is_dir()).collect()
191}
192
193fn default_man_dirs() -> Vec<PathBuf> {
194 let base_patterns = [
195 "/usr/share/man/man",
196 "/usr/local/share/man/man",
197 "/usr/local/man/man",
198 ];
199 let sections = ["1", "2", "3", "4", "5", "6", "7", "8", "1p", "3p"];
200
201 let mut dirs = Vec::new();
202 for base in &base_patterns {
203 for section in §ions {
204 let dir = PathBuf::from(format!("{base}{section}"));
205 if dir.is_dir() {
206 dirs.push(dir);
207 }
208 }
209 }
210
211 if let Ok(manpath) = std::env::var("MANPATH") {
212 for p in manpath.split(':') {
213 if !p.is_empty() {
214 let base = PathBuf::from(p);
215 if base.is_dir() {
216 for section in §ions {
217 let dir = base.join(format!("man{section}"));
218 if dir.is_dir() && !dirs.contains(&dir) {
219 dirs.push(dir);
220 }
221 }
222 }
223 }
224 }
225 }
226
227 dirs
228}
229
230fn default_src_dirs() -> Vec<PathBuf> {
231 let mut dirs = Vec::new();
232 for base in &["/usr/src", "/usr/local/src"] {
233 let base_path = Path::new(base);
234 if base_path.is_dir()
235 && let Ok(entries) = fs::read_dir(base_path)
236 {
237 for entry in entries.flatten() {
238 let path = entry.path();
239 if path.is_dir() {
240 dirs.push(path);
241 }
242 }
243 }
244 }
245 dirs
246}
247
248fn is_executable(path: &Path) -> bool {
249 path.metadata()
250 .map(|m| m.permissions().mode() & 0o111 != 0 && m.is_file())
251 .unwrap_or(false)
252}
253
254fn strip_name(name: &str) -> &str {
255 let name = name.rsplit('/').next().unwrap_or(name);
256 name.strip_prefix("s.").unwrap_or(name)
257}
258
259fn matches_name(filename: &str, name: &str, glob_mode: bool) -> bool {
260 if glob_mode {
261 glob_match(name, filename)
262 } else {
263 filename == name
264 }
265}
266
267fn matches_name_with_ext(filename: &str, name: &str, glob_mode: bool) -> bool {
268 if glob_mode {
269 let base = filename.split('.').next().unwrap_or(filename);
270 glob_match(name, base)
271 } else {
272 filename == name || filename.starts_with(&format!("{name}."))
273 }
274}
275
276fn glob_match(pattern: &str, text: &str) -> bool {
277 let pat: Vec<char> = pattern.chars().collect();
278 let txt: Vec<char> = text.chars().collect();
279 glob_match_inner(&pat, &txt, 0, 0)
280}
281
282fn glob_match_inner(pat: &[char], txt: &[char], pi: usize, ti: usize) -> bool {
283 if pi == pat.len() {
284 return ti == txt.len();
285 }
286 if pat[pi] == '*' {
287 for t in ti..=txt.len() {
288 if glob_match_inner(pat, txt, pi + 1, t) {
289 return true;
290 }
291 }
292 return false;
293 }
294 if pat[pi] == '?' {
295 if ti < txt.len() {
296 return glob_match_inner(pat, txt, pi + 1, ti + 1);
297 }
298 return false;
299 }
300 if ti < txt.len() && pat[pi] == txt[ti] {
301 return glob_match_inner(pat, txt, pi + 1, ti + 1);
302 }
303 false
304}
305
306fn find_binaries(
307 name: &str,
308 dirs: &[PathBuf],
309 glob_mode: bool,
310) -> Vec<PathBuf> {
311 let mut results = BTreeSet::new();
312 for dir in dirs {
313 if let Ok(entries) = fs::read_dir(dir) {
314 for entry in entries.flatten() {
315 let path = entry.path();
316 if let Some(fname) = path.file_name().and_then(|f| f.to_str())
317 && matches_name(fname, name, glob_mode)
318 && is_executable(&path)
319 {
320 results.insert(path);
321 }
322 }
323 }
324 }
325 results.into_iter().collect()
326}
327
328fn find_manuals(name: &str, dirs: &[PathBuf], glob_mode: bool) -> Vec<PathBuf> {
329 find_files_with_ext(name, dirs, glob_mode)
330}
331
332fn find_sources(name: &str, dirs: &[PathBuf], glob_mode: bool) -> Vec<PathBuf> {
333 find_files_with_ext(name, dirs, glob_mode)
334}
335
336fn find_files_with_ext(
337 name: &str,
338 dirs: &[PathBuf],
339 glob_mode: bool,
340) -> Vec<PathBuf> {
341 let mut results = BTreeSet::new();
342 for dir in dirs {
343 if let Ok(entries) = fs::read_dir(dir) {
344 for entry in entries.flatten() {
345 let path = entry.path();
346 if let Some(fname) = path.file_name().and_then(|f| f.to_str())
347 && matches_name_with_ext(fname, name, glob_mode)
348 && path.is_file()
349 {
350 results.insert(path);
351 }
352 }
353 }
354 }
355 results.into_iter().collect()
356}
357
358pub fn run(args: Args) -> ExitCode {
359 let search_all =
360 !args.search_binaries && !args.search_manuals && !args.search_sources;
361 let do_bins = search_all || args.search_binaries;
362 let do_mans = search_all || args.search_manuals;
363 let do_srcs = search_all || args.search_sources;
364
365 let bin_dirs = if !args.custom_bin_dirs.is_empty() {
366 args.custom_bin_dirs.clone()
367 } else {
368 default_bin_dirs()
369 };
370
371 let man_dirs = if !args.custom_man_dirs.is_empty() {
372 args.custom_man_dirs.clone()
373 } else {
374 default_man_dirs()
375 };
376
377 let src_dirs = if !args.custom_src_dirs.is_empty() {
378 args.custom_src_dirs.clone()
379 } else {
380 default_src_dirs()
381 };
382
383 if args.list_paths {
384 if do_bins {
385 println!("bin: {}", format_dir_list(&bin_dirs));
386 }
387 if do_mans {
388 println!("man: {}", format_dir_list(&man_dirs));
389 }
390 if do_srcs {
391 println!("src: {}", format_dir_list(&src_dirs));
392 }
393 return ExitCode::SUCCESS;
394 }
395
396 if args.names.is_empty() {
397 eprintln!("whereis: no name specified");
398 return ExitCode::FAILURE;
399 }
400
401 for name in &args.names {
402 let name = strip_name(name);
403 let mut paths: Vec<PathBuf> = Vec::new();
404
405 let bin_results = if do_bins {
406 find_binaries(name, &bin_dirs, args.glob_mode)
407 } else {
408 Vec::new()
409 };
410 let man_results = if do_mans {
411 find_manuals(name, &man_dirs, args.glob_mode)
412 } else {
413 Vec::new()
414 };
415 let src_results = if do_srcs {
416 find_sources(name, &src_dirs, args.glob_mode)
417 } else {
418 Vec::new()
419 };
420
421 if args.unusual_only {
422 let mut expected_types = 0;
423 let mut found_single = 0;
424 if do_bins {
425 expected_types += 1;
426 if bin_results.len() == 1 {
427 found_single += 1;
428 }
429 }
430 if do_mans {
431 expected_types += 1;
432 if man_results.len() == 1 {
433 found_single += 1;
434 }
435 }
436 if do_srcs {
437 expected_types += 1;
438 if src_results.len() == 1 {
439 found_single += 1;
440 }
441 }
442 if found_single == expected_types {
443 continue;
444 }
445 }
446
447 paths.extend(bin_results);
448 paths.extend(man_results);
449 paths.extend(src_results);
450
451 let path_strs: Vec<String> = paths
452 .iter()
453 .map(|p| p.to_string_lossy().to_string())
454 .collect();
455
456 if path_strs.is_empty() {
457 println!("{name}:");
458 } else {
459 println!("{name}: {}", path_strs.join(" "));
460 }
461 }
462
463 ExitCode::SUCCESS
464}
465
466fn format_dir_list(dirs: &[PathBuf]) -> String {
467 dirs.iter()
468 .map(|d| d.to_string_lossy().to_string())
469 .collect::<Vec<_>>()
470 .join(" ")
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 #[test]
478 fn test_strip_name_simple() {
479 assert_eq!(strip_name("ls"), "ls");
480 }
481
482 #[test]
483 fn test_strip_name_with_path() {
484 assert_eq!(strip_name("/usr/bin/ls"), "ls");
485 }
486
487 #[test]
488 fn test_strip_name_sccs_prefix() {
489 assert_eq!(strip_name("s.main.c"), "main.c");
490 }
491
492 #[test]
493 fn test_glob_match_exact() {
494 assert!(glob_match("ls", "ls"));
495 assert!(!glob_match("ls", "lsblk"));
496 }
497
498 #[test]
499 fn test_glob_match_star() {
500 assert!(glob_match("ls*", "ls"));
501 assert!(glob_match("ls*", "lsblk"));
502 assert!(!glob_match("ls*", "cat"));
503 }
504
505 #[test]
506 fn test_glob_match_question() {
507 assert!(glob_match("l?", "ls"));
508 assert!(!glob_match("l?", "lsblk"));
509 }
510
511 #[test]
512 fn test_matches_name_with_ext_exact() {
513 assert!(matches_name_with_ext("ls.1", "ls", false));
514 assert!(matches_name_with_ext("ls.1.gz", "ls", false));
515 assert!(!matches_name_with_ext("lsblk.1", "ls", false));
516 }
517
518 #[test]
519 fn test_default_bin_dirs_not_empty() {
520 let dirs = default_bin_dirs();
521 assert!(!dirs.is_empty());
522 }
523}