#[cfg(not(unix))]
fn main() {
eprintln!("who: only available on Unix");
std::process::exit(1);
}
#[cfg(unix)]
use std::process;
#[cfg(unix)]
use coreutils_rs::who;
#[cfg(unix)]
const TOOL_NAME: &str = "who";
#[cfg(unix)]
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(unix)]
fn print_help() {
println!("Usage: {} [OPTION]... [ FILE | ARG1 ARG2 ]", TOOL_NAME);
println!("Show who is logged on.");
println!();
println!(" -a, --all same as -b -d --login -p -r -t -T -u");
println!(" -b, --boot time of last system boot");
println!(" -d, --dead print dead processes");
println!(" -H, --heading print line of column headings");
println!(" -l, --login print system login processes");
println!(" -m only hostname and user associated with stdin");
println!(" -p, --process print active processes spawned by init");
println!(" -q, --count all login names and number of users logged on");
println!(" -r, --runlevel print current runlevel");
println!(" -s, --short print only name, line, and time (default)");
println!(" -t, --time print last system clock change");
println!(" -T, -w, --mesg add user's message status as +, - or ?");
println!(" -u, --users list users logged in");
println!(" --ips print ips instead of hostnames");
println!(" --lookup attempt to canonicalize hostnames via DNS");
println!(" --help display this help and exit");
println!(" --version output version information and exit");
println!();
println!(
"If ARG1 ARG2 given (e.g. 'who am i'), print only the entry for the current terminal."
);
}
#[cfg(unix)]
fn main() {
coreutils_rs::common::reset_sigpipe();
let mut config = who::WhoConfig::default();
let mut positional: Vec<String> = Vec::new();
for arg in std::env::args_os().skip(1) {
let arg = arg.to_string_lossy();
match arg.as_ref() {
"--help" => {
print_help();
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"-a" | "--all" => {
config.show_all = true;
config.apply_all();
}
"-b" | "--boot" => config.show_boot = true,
"-d" | "--dead" => config.show_dead = true,
"-H" | "--heading" => config.show_heading = true,
"-l" | "--login" => config.show_login = true,
"-m" => config.only_current = true,
"-p" | "--process" => config.show_init_spawn = true,
"-q" | "--count" => config.show_count = true,
"-r" | "--runlevel" => config.show_runlevel = true,
"-s" | "--short" => config.short_format = true,
"-t" | "--time" => config.show_clock_change = true,
"-T" | "-w" | "--mesg" => config.show_mesg = true,
"-u" | "--users" => config.show_users = true,
"--ips" => config.show_ips = true,
"--lookup" => config.show_lookup = true,
s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => {
for ch in s[1..].chars() {
match ch {
'a' => {
config.show_all = true;
config.apply_all();
}
'b' => config.show_boot = true,
'd' => config.show_dead = true,
'H' => config.show_heading = true,
'l' => config.show_login = true,
'm' => config.only_current = true,
'p' => config.show_init_spawn = true,
'q' => config.show_count = true,
'r' => config.show_runlevel = true,
's' => config.short_format = true,
't' => config.show_clock_change = true,
'T' | 'w' => config.show_mesg = true,
'u' => config.show_users = true,
_ => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, ch);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
}
s if s.starts_with("--") => {
eprintln!("{}: unrecognized option '{}'", TOOL_NAME, s);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
_ => positional.push(arg.into_owned()),
}
}
if positional.len() > 2 {
eprintln!("{}: extra operand '{}'", TOOL_NAME, positional[2]);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
if positional.len() == 2 {
let a = positional[0].to_lowercase();
let b = positional[1].to_lowercase();
if a == "am" && b == "i" {
config.am_i = true;
}
}
let output = who::run_who(&config);
if !output.is_empty() {
println!("{}", output);
}
process::exit(0);
}
#[cfg(test)]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fwho");
Command::new(path)
}
#[cfg(unix)]
#[test]
fn test_who_runs() {
let output = cmd().output().unwrap();
assert!(
output.status.success(),
"fwho should exit with code 0, stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[cfg(unix)]
#[test]
fn test_who_heading() {
let output = cmd().arg("-H").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("NAME"), "Heading should contain NAME");
assert!(stdout.contains("LINE"), "Heading should contain LINE");
assert!(stdout.contains("TIME"), "Heading should contain TIME");
}
#[cfg(unix)]
#[test]
fn test_who_count() {
let output = cmd().arg("-q").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("# users="),
"Count mode should show '# users=N', got: {}",
stdout
);
}
#[cfg(unix)]
#[test]
fn test_who_boot() {
let output = cmd().arg("-b").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
if !stdout.trim().is_empty() {
assert!(
stdout.contains("system boot"),
"Boot output should contain 'system boot', got: {}",
stdout
);
}
}
#[cfg(unix)]
#[test]
fn test_who_format_check() {
let output = cmd().output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
let parts: Vec<&str> = line.split_whitespace().collect();
assert!(
parts.len() >= 3,
"Output line should have at least 3 fields: '{}'",
line
);
}
}
#[cfg(unix)]
#[test]
fn test_who_matches_gnu_format() {
let gnu = Command::new("who").output();
if let Ok(gnu) = gnu {
let ours = cmd().output().unwrap();
assert_eq!(
ours.status.code(),
gnu.status.code(),
"Exit code mismatch: ours={:?} gnu={:?}",
ours.status.code(),
gnu.status.code()
);
let gnu_lines = String::from_utf8_lossy(&gnu.stdout).lines().count();
let our_lines = String::from_utf8_lossy(&ours.stdout).lines().count();
assert_eq!(
our_lines, gnu_lines,
"Line count mismatch: ours={} gnu={}",
our_lines, gnu_lines
);
}
}
#[cfg(unix)]
#[test]
fn test_who_basic() {
let output = cmd().output().unwrap();
assert!(output.status.success());
}
#[cfg(unix)]
#[test]
fn test_who_count_exit_success() {
let output = cmd().arg("-q").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains('#') || stdout.contains("="));
}
#[cfg(unix)]
#[test]
fn test_who_heading_long_flag() {
let output = cmd().arg("--heading").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("NAME") || stdout.is_empty());
}
#[cfg(unix)]
#[test]
fn test_who_boot_exit_success() {
let output = cmd().arg("-b").output().unwrap();
assert!(output.status.success());
}
#[cfg(unix)]
#[test]
fn test_who_am_i() {
let output = cmd().args(["am", "i"]).output().unwrap();
assert!(output.status.success());
}
#[cfg(unix)]
#[test]
fn test_who_invalid_option_exits_nonzero() {
let output = cmd().arg("--invalid").output().unwrap();
assert!(
!output.status.success(),
"fwho --invalid should exit with non-zero status"
);
}
#[cfg(unix)]
#[test]
fn test_who_invalid_option_stderr() {
let output = cmd().arg("--invalid").output().unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("unrecognized option"),
"stderr should contain 'unrecognized option', got: {}",
stderr
);
}
}