use std::env;
use std::fs;
use std::io::{self, BufRead, BufWriter, Write};
use std::path::{Path, PathBuf};
use std::process;
mod cli;
mod executor;
mod output;
mod path;
mod shell_integration;
use cli::{Args, ColorWhen};
use executor::{ExecutableCheck, SearchResult};
use output::OutputFormatter;
use path::PathSearcher;
fn main() {
let args = match Args::parse() {
Ok(args) => args,
Err(e) => {
eprintln!("Error: {e}");
process::exit(2);
}
};
if args.silent {
let result = run(&args);
process::exit(result);
}
match run(&args) {
0 => process::exit(0),
code => process::exit(code),
}
}
#[allow(clippy::too_many_lines)]
fn run(args: &Args) -> i32 {
if let Some(ref shell) = args.init_shell {
match shell_integration::generate_init_script(shell) {
Ok(script) => {
print!("{script}");
return 0;
}
Err(e) => {
eprintln!("Error: {e}");
return 2;
}
}
}
let path_var = match &args.path_override {
Some(p) => p.clone(),
None => env::var("PATH").unwrap_or_default(),
};
let searcher = PathSearcher::new(&path_var);
let stdout = io::stdout();
let mut out = BufWriter::new(stdout.lock());
if let Some((from, to)) = args.move_indices {
match searcher.move_entry(from, to) {
Ok(new_path) => {
writeln!(out, "{new_path}").ok();
out.flush().ok();
return 0;
}
Err(e) => {
if !args.silent {
eprintln!("Error: {e}");
}
return 2;
}
}
}
if let Some((idx1, idx2)) = args.swap_indices {
match searcher.swap_entries(idx1, idx2) {
Ok(new_path) => {
writeln!(out, "{new_path}").ok();
out.flush().ok();
return 0;
}
Err(e) => {
if !args.silent {
eprintln!("Error: {e}");
}
return 2;
}
}
}
if let Some((ref name, target_idx)) = args.prefer_target {
return handle_prefer(&searcher, name, target_idx, args, &mut out);
}
let names = get_names(args);
if names.is_empty() {
let num_dirs = searcher.dirs().len();
if num_dirs > 999 {
if !args.silent {
eprintln!("Error: PATH has {num_dirs} entries (max 999 supported)");
}
return 3;
}
for (idx, dir) in searcher.dirs().iter().enumerate() {
if args.index {
writeln!(out, "{:>4} {}", format!("[{}]", idx + 1), dir.display()).ok();
} else {
writeln!(out, "{}", dir.display()).ok();
}
}
out.flush().ok();
return 0;
}
let mut all_found = true;
let stderr = io::stderr();
let mut err = BufWriter::new(stderr.lock());
let use_color = should_use_color(args);
let mut formatter = OutputFormatter::new(use_color, args.print0);
for name in names {
let results = search_name(&searcher, &name, args);
if results.is_empty() {
all_found = false;
if !args.silent && !args.quiet {
writeln!(err, "{name}: not found").ok();
}
continue;
}
let max_index = results.iter().map(|r| r.path_index).max().unwrap_or(0);
if max_index > 999 {
if !args.silent {
eprintln!("Error: PATH index {max_index} exceeds max 999");
}
return 3;
}
for (i, result) in results.iter().enumerate() {
let is_winner = i == 0;
formatter
.write_result(
&mut out,
result,
is_winner,
args.follow_symlinks,
args.index,
3, )
.ok();
if (!args.all && !args.full) || args.one {
break;
}
}
if args.full {
writeln!(out).ok();
let match_indices: std::collections::HashSet<usize> =
results.iter().map(|r| r.path_index).collect();
for (idx, dir) in searcher.dirs().iter().enumerate() {
let path_index = idx + 1;
let has_match = match_indices.contains(&path_index);
if args.index {
write!(out, "{:>4} ", format!("[{}]", path_index)).ok();
}
if use_color && has_match {
writeln!(out, "\x1b[33m{}\x1b[0m", dir.display()).ok();
} else {
writeln!(out, "{}", dir.display()).ok();
}
}
}
}
out.flush().ok();
err.flush().ok();
i32::from(!all_found)
}
fn get_names(args: &Args) -> Vec<String> {
if !args.names.is_empty() {
return args.names.clone();
}
if !atty::is(atty::Stream::Stdin) {
let stdin = io::stdin();
let mut names = Vec::new();
for line in stdin.lock().lines().map_while(Result::ok) {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with('#') {
names.push(trimmed.to_string());
}
}
return names;
}
Vec::new()
}
fn search_name(searcher: &PathSearcher, name: &str, args: &Args) -> Vec<SearchResult> {
if name.contains('/') {
let path = PathBuf::from(name);
if let Some(result) = check_path(&path, args, 0) {
return vec![result];
}
return vec![];
}
let mut results = Vec::new();
for (idx, dir) in searcher.dirs().iter().enumerate() {
let candidate = dir.join(name);
if let Some(result) = check_path(&candidate, args, idx + 1) {
results.push(result);
}
}
results
}
fn check_path(path: &Path, args: &Args, path_index: usize) -> Option<SearchResult> {
let checker = ExecutableCheck::new(path);
if !checker.exists() {
return None;
}
let is_executable = checker.is_executable();
if !is_executable && !args.show_nonexec {
return None;
}
let canonical_path = if args.follow_symlinks {
fs::canonicalize(path).ok()
} else {
None
};
let metadata = if args.stat {
checker.get_file_metadata()
} else {
None
};
Some(SearchResult {
path: path.to_path_buf(),
canonical_path,
metadata,
path_index,
})
}
fn should_use_color(args: &Args) -> bool {
match args.color {
ColorWhen::Always => true,
ColorWhen::Never => false,
ColorWhen::Auto => atty::is(atty::Stream::Stdout),
}
}
fn handle_prefer<W: Write>(
searcher: &PathSearcher,
name: &str,
target_idx: usize,
args: &Args,
out: &mut W,
) -> i32 {
let results = search_name(searcher, name, args);
if results.is_empty() {
if !args.silent {
eprintln!("Error: {name}: not found");
}
return 1;
}
let winner_idx = results[0].path_index;
let target_result = results.iter().find(|r| r.path_index == target_idx);
if target_result.is_none() {
if !args.silent {
eprintln!("Error: {name} not found at index {target_idx}");
}
return 2;
}
let new_position = if target_idx > winner_idx {
winner_idx
} else {
if !args.silent {
eprintln!(
"Error: {name} at index {target_idx} is already preferred over index {winner_idx}"
);
}
return 2;
};
match searcher.move_entry(target_idx, new_position) {
Ok(new_path) => {
writeln!(out, "{new_path}").ok();
out.flush().ok();
0
}
Err(e) => {
if !args.silent {
eprintln!("Error: {e}");
}
2
}
}
}
mod atty {
use std::os::unix::io::AsRawFd;
pub fn is(stream: Stream) -> bool {
let fd = match stream {
Stream::Stdout => std::io::stdout().as_raw_fd(),
Stream::Stdin => std::io::stdin().as_raw_fd(),
};
unsafe { libc::isatty(fd) == 1 }
}
#[derive(Copy, Clone)]
pub enum Stream {
Stdout,
Stdin,
}
}