use std::env;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ColorWhen {
Auto,
Never,
Always,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct Args {
pub names: Vec<String>,
pub all: bool,
pub full: bool,
pub follow_symlinks: bool,
pub print0: bool,
pub quiet: bool,
pub silent: bool,
pub one: bool,
pub show_nonexec: bool,
pub path_override: Option<String>,
pub color: ColorWhen,
pub stat: bool,
pub index: bool,
pub move_indices: Option<(usize, usize)>,
pub swap_indices: Option<(usize, usize)>,
pub prefer_target: Option<(String, usize)>,
pub init_shell: Option<String>,
}
impl Args {
#[allow(clippy::too_many_lines)]
pub fn parse() -> Result<Self, String> {
let mut args = Args {
names: Vec::new(),
all: false,
full: false,
follow_symlinks: false,
print0: false,
quiet: false,
silent: false,
one: false,
show_nonexec: false,
path_override: None,
color: ColorWhen::Auto,
stat: false,
index: false,
move_indices: None,
swap_indices: None,
prefer_target: None,
init_shell: None,
};
let args_vec: Vec<String> = env::args().skip(1).collect();
if let Some(idx) = args_vec.iter().position(|a| a == "init") {
if idx + 1 >= args_vec.len() {
return Err("init requires a shell argument (bash, zsh, fish)".to_string());
}
args.init_shell = Some(args_vec[idx + 1].clone());
return Ok(args);
}
let mut expect_path = false;
let mut expect_color = false;
let mut expect_move_from = false;
let mut expect_swap_from = false;
let mut expect_prefer_name = false;
let mut move_from: Option<usize> = None;
let mut swap_from: Option<usize> = None;
let mut prefer_name: Option<String> = None;
for arg in args_vec {
if expect_path {
args.path_override = Some(arg);
expect_path = false;
continue;
}
if expect_color {
args.color = Self::parse_color(&arg)?;
expect_color = false;
continue;
}
if expect_move_from {
let from = arg
.parse::<usize>()
.map_err(|_| format!("Invalid move source index: {arg}"))?;
move_from = Some(from);
expect_move_from = false;
continue;
}
if let Some(from) = move_from {
let to = arg
.parse::<usize>()
.map_err(|_| format!("Invalid move destination index: {arg}"))?;
args.move_indices = Some((from, to));
move_from = None;
continue;
}
if expect_swap_from {
let idx1 = arg
.parse::<usize>()
.map_err(|_| format!("Invalid swap first index: {arg}"))?;
swap_from = Some(idx1);
expect_swap_from = false;
continue;
}
if let Some(idx1) = swap_from {
let idx2 = arg
.parse::<usize>()
.map_err(|_| format!("Invalid swap second index: {arg}"))?;
args.swap_indices = Some((idx1, idx2));
swap_from = None;
continue;
}
if expect_prefer_name {
prefer_name = Some(arg);
expect_prefer_name = false;
continue;
}
if let Some(name) = prefer_name {
let idx = arg
.parse::<usize>()
.map_err(|_| format!("Invalid prefer index: {arg}"))?;
args.prefer_target = Some((name, idx));
prefer_name = None;
continue;
}
Self::process_arg(
&mut args,
&arg,
&mut expect_path,
&mut expect_color,
&mut expect_move_from,
&mut expect_swap_from,
&mut expect_prefer_name,
)?;
}
if expect_path {
return Err("--path requires a value".to_string());
}
if expect_color {
return Err("--color requires a value".to_string());
}
if expect_move_from || move_from.is_some() {
return Err("--move requires two indices: FROM TO".to_string());
}
if expect_swap_from || swap_from.is_some() {
return Err("--swap requires two indices: IDX1 IDX2".to_string());
}
if expect_prefer_name || prefer_name.is_some() {
return Err("--prefer requires NAME and INDEX".to_string());
}
Ok(args)
}
fn parse_color(val: &str) -> Result<ColorWhen, String> {
match val {
"auto" => Ok(ColorWhen::Auto),
"never" => Ok(ColorWhen::Never),
"always" => Ok(ColorWhen::Always),
_ => Err(format!("Invalid color value: {val}")),
}
}
fn process_arg(
args: &mut Args,
arg: &str,
expect_path: &mut bool,
expect_color: &mut bool,
expect_move_from: &mut bool,
expect_swap_from: &mut bool,
expect_prefer_name: &mut bool,
) -> Result<(), String> {
match arg {
"-a" | "--all" => args.all = true,
"-f" | "--full" => args.full = true,
"-i" | "--index" => args.index = true,
"-l" | "-L" | "--follow-symlinks" => args.follow_symlinks = true,
"--move" => *expect_move_from = true,
"--swap" => *expect_swap_from = true,
"--prefer" => *expect_prefer_name = true,
"-o" | "--one" => args.one = true,
"-0" | "--print0" => args.print0 = true,
"-q" | "--quiet" => args.quiet = true,
"-s" | "--stat" => args.stat = true,
"--silent" => args.silent = true,
"--show-nonexec" => args.show_nonexec = true,
"--path" => *expect_path = true,
"--color" => *expect_color = true,
"-h" | "--help" => {
print_help();
std::process::exit(0);
}
s if s.starts_with("--path=") => {
args.path_override = Some(s.trim_start_matches("--path=").to_string());
}
s if s.starts_with("--color=") => {
let val = s.trim_start_matches("--color=");
args.color = Self::parse_color(val)?;
}
s if s.starts_with('-') && !s.starts_with("--") && s.len() > 2 => {
Self::parse_combined_flags(args, s)?;
}
s if s.starts_with('-') => {
return Err(format!("Unknown option: {s}"));
}
_ => args.names.push(arg.to_string()),
}
Ok(())
}
fn parse_combined_flags(args: &mut Args, s: &str) -> Result<(), String> {
for ch in s[1..].chars() {
match ch {
'a' => args.all = true,
'f' => args.full = true,
'i' => args.index = true,
'l' | 'L' => args.follow_symlinks = true,
'o' => args.one = true,
's' => args.stat = true,
'0' => args.print0 = true,
'q' => args.quiet = true,
'h' => {
print_help();
std::process::exit(0);
}
_ => return Err(format!("Unknown flag: -{ch}")),
}
}
Ok(())
}
}
fn print_help() {
println!(
"whi - magically simple PATH management
USAGE:
whi [FLAGS] [OPTIONS] <NAME>...
whi [FLAGS] [OPTIONS] # names from stdin
whi --move <FROM> <TO> # reorder PATH
whi --swap <IDX1> <IDX2> # swap PATH entries
whi --prefer <NAME> <INDEX> # prefer executable at INDEX
whi init <SHELL> # output shell integration
FLAGS:
-a, --all Show all PATH matches (default: only winner)
-f, --full Show all matches + full PATH listing (implies -a)
-i, --index Show PATH index next to each entry
-l, -L, --follow-symlinks
Resolve and show canonical targets
-s, --stat Include inode/device/mtime/size metadata
-0, --print0 NUL-separated output for xargs
-q, --quiet Suppress non-fatal stderr warnings
--silent Print nothing to stderr, use exit codes only
-o, --one Only print the first match per name
--show-nonexec Also list files that exist but aren't executable
-h, --help Print help information
PATH MANIPULATION:
--move <FROM> <TO> Move PATH entry from index FROM to index TO
--swap <IDX1> <IDX2>
Swap PATH entries at indices IDX1 and IDX2
--prefer <NAME> <INDEX>
Make executable NAME at INDEX win by moving it
just before the current winner (minimal change)
OPTIONS:
--path <PATH> Override environment PATH string
--color <WHEN> Colorize output: auto, never, always [default: auto]
SHELL INTEGRATION:
whi init bash Output bash integration code
whi init zsh Output zsh integration code
whi init fish Output fish integration code
Add to your shell config:
bash/zsh: eval \"$(whi init bash)\" or eval \"$(whi init zsh)\"
fish: whi init fish | source
Provides shell commands to manipulate PATH in current shell:
whim 10 1 # Move PATH entry 10 to position 1
whis 10 41 # Swap PATH entries 10 and 41
whip cargo 50 # Make cargo at index 50 the winner
whia cargo # Show all cargo matches with indices (whi -ia)
whii # Show all PATH entries with indices (whi -i)
EXIT CODES:
0 All names found
1 At least one not found
2 Usage error
3 I/O or environment error"
);
}