diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 8ac8f6c84..6b393b905 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -5,21 +5,11 @@ // spell-checker:ignore (ToDO) BUFSIZE gecos fullname, mesg iobuf -use uucore::entries::{Locate, Passwd}; -use uucore::error::{FromIo, UResult}; -use uucore::libc::S_IWGRP; -use uucore::utmpx::{self, time, Utmpx}; - -use std::io::prelude::*; -use std::io::BufReader; - -use std::fs::File; -use std::os::unix::fs::MetadataExt; - use clap::{crate_version, Arg, ArgAction, Command}; -use std::path::PathBuf; use uucore::{format_usage, help_about, help_usage}; +mod platform; + const ABOUT: &str = help_about!("pinky.md"); const USAGE: &str = help_usage!("pinky.md"); @@ -37,88 +27,8 @@ mod options { pub const HELP: &str = "help"; } -fn get_long_usage() -> String { - format!( - "A lightweight 'finger' program; print user information.\n\ - The utmp file will be {}.", - utmpx::DEFAULT_FILE - ) -} - #[uucore::main] -pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let args = args.collect_ignore(); - - let matches = uu_app() - .after_help(get_long_usage()) - .try_get_matches_from(args)?; - - let users: Vec = matches - .get_many::(options::USER) - .map(|v| v.map(ToString::to_string).collect()) - .unwrap_or_default(); - - // If true, display the hours:minutes since each user has touched - // the keyboard, or blank if within the last minute, or days followed - // by a 'd' if not within the last day. - let mut include_idle = true; - - // If true, display a line at the top describing each field. - let include_heading = !matches.get_flag(options::OMIT_HEADINGS); - - // if true, display the user's full name from pw_gecos. - let mut include_fullname = true; - - // if true, display the user's ~/.project file when doing long format. - let include_project = !matches.get_flag(options::OMIT_PROJECT_FILE); - - // if true, display the user's ~/.plan file when doing long format. - let include_plan = !matches.get_flag(options::OMIT_PLAN_FILE); - - // if true, display the user's home directory and shell - // when doing long format. - let include_home_and_shell = !matches.get_flag(options::OMIT_HOME_DIR); - - // if true, use the "short" output format. - let do_short_format = !matches.get_flag(options::LONG_FORMAT); - - /* if true, display the ut_host field. */ - let mut include_where = true; - - if matches.get_flag(options::OMIT_NAME) { - include_fullname = false; - } - if matches.get_flag(options::OMIT_NAME_HOST) { - include_fullname = false; - include_where = false; - } - if matches.get_flag(options::OMIT_NAME_HOST_TIME) { - include_fullname = false; - include_idle = false; - include_where = false; - } - - let pk = Pinky { - include_idle, - include_heading, - include_fullname, - include_project, - include_plan, - include_home_and_shell, - include_where, - names: users, - }; - - if do_short_format { - match pk.short_pinky() { - Ok(_) => Ok(()), - Err(e) => Err(e.map_err_context(String::new)), - } - } else { - pk.long_pinky(); - Ok(()) - } -} +use platform::uumain; pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -197,17 +107,6 @@ pub fn uu_app() -> Command { ) } -struct Pinky { - include_idle: bool, - include_heading: bool, - include_fullname: bool, - include_project: bool, - include_plan: bool, - include_where: bool, - include_home_and_shell: bool, - names: Vec, -} - pub trait Capitalize { fn capitalize(&self) -> String; } @@ -225,180 +124,3 @@ impl Capitalize for str { }) } } - -fn idle_string(when: i64) -> String { - thread_local! { - static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap(); - } - NOW.with(|n| { - let duration = n.unix_timestamp() - when; - if duration < 60 { - // less than 1min - " ".to_owned() - } else if duration < 24 * 3600 { - // less than 1day - let hours = duration / (60 * 60); - let minutes = (duration % (60 * 60)) / 60; - format!("{hours:02}:{minutes:02}") - } else { - // more than 1day - let days = duration / (24 * 3600); - format!("{days}d") - } - }) -} - -fn time_string(ut: &Utmpx) -> String { - // "%b %e %H:%M" - let time_format: Vec = - time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]") - .unwrap(); - ut.login_time().format(&time_format).unwrap() // LC_ALL=C -} - -fn gecos_to_fullname(pw: &Passwd) -> Option { - let mut gecos = if let Some(gecos) = &pw.user_info { - gecos.clone() - } else { - return None; - }; - if let Some(n) = gecos.find(',') { - gecos.truncate(n); - } - Some(gecos.replace('&', &pw.name.capitalize())) -} - -impl Pinky { - fn print_entry(&self, ut: &Utmpx) -> std::io::Result<()> { - let mut pts_path = PathBuf::from("/dev"); - pts_path.push(ut.tty_device().as_str()); - - let mesg; - let last_change; - - match pts_path.metadata() { - #[allow(clippy::unnecessary_cast)] - Ok(meta) => { - mesg = if meta.mode() & S_IWGRP as u32 == 0 { - '*' - } else { - ' ' - }; - last_change = meta.atime(); - } - _ => { - mesg = '?'; - last_change = 0; - } - } - - print!("{1:<8.0$}", utmpx::UT_NAMESIZE, ut.user()); - - if self.include_fullname { - let fullname = if let Ok(pw) = Passwd::locate(ut.user().as_ref()) { - gecos_to_fullname(&pw) - } else { - None - }; - if let Some(fullname) = fullname { - print!(" {fullname:<19.19}"); - } else { - print!(" {:19}", " ???"); - } - } - - print!(" {}{:<8.*}", mesg, utmpx::UT_LINESIZE, ut.tty_device()); - - if self.include_idle { - if last_change == 0 { - print!(" {:<6}", "?????"); - } else { - print!(" {:<6}", idle_string(last_change)); - } - } - - print!(" {}", time_string(ut)); - - let mut s = ut.host(); - if self.include_where && !s.is_empty() { - s = ut.canon_host()?; - print!(" {s}"); - } - - println!(); - Ok(()) - } - - fn print_heading(&self) { - print!("{:<8}", "Login"); - if self.include_fullname { - print!(" {:<19}", "Name"); - } - print!(" {:<9}", " TTY"); - if self.include_idle { - print!(" {:<6}", "Idle"); - } - print!(" {:<16}", "When"); - if self.include_where { - print!(" Where"); - } - println!(); - } - - fn short_pinky(&self) -> std::io::Result<()> { - if self.include_heading { - self.print_heading(); - } - for ut in Utmpx::iter_all_records() { - if ut.is_user_process() - && (self.names.is_empty() || self.names.iter().any(|n| n.as_str() == ut.user())) - { - self.print_entry(&ut)?; - } - } - Ok(()) - } - - fn long_pinky(&self) { - for u in &self.names { - print!("Login name: {u:<28}In real life: "); - if let Ok(pw) = Passwd::locate(u.as_str()) { - let fullname = gecos_to_fullname(&pw).unwrap_or_default(); - let user_dir = pw.user_dir.unwrap_or_default(); - let user_shell = pw.user_shell.unwrap_or_default(); - println!(" {fullname}"); - if self.include_home_and_shell { - print!("Directory: {user_dir:<29}"); - println!("Shell: {user_shell}"); - } - if self.include_project { - let mut p = PathBuf::from(&user_dir); - p.push(".project"); - if let Ok(f) = File::open(p) { - print!("Project: "); - read_to_console(f); - } - } - if self.include_plan { - let mut p = PathBuf::from(&user_dir); - p.push(".plan"); - if let Ok(f) = File::open(p) { - println!("Plan:"); - read_to_console(f); - } - } - println!(); - } else { - println!(" ???"); - } - } - } -} - -fn read_to_console(f: F) { - let mut reader = BufReader::new(f); - let mut iobuf = Vec::new(); - if reader.read_to_end(&mut iobuf).is_ok() { - print!("{}", String::from_utf8_lossy(&iobuf)); - } -} diff --git a/src/uu/pinky/src/platform/mod.rs b/src/uu/pinky/src/platform/mod.rs new file mode 100644 index 000000000..e0e87dca1 --- /dev/null +++ b/src/uu/pinky/src/platform/mod.rs @@ -0,0 +1,14 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(not(target_os = "openbsd"))] +mod unix; +#[cfg(not(target_os = "openbsd"))] +pub use self::unix::*; + +#[cfg(target_os = "openbsd")] +mod openbsd; +#[cfg(target_os = "openbsd")] +pub use self::openbsd::*; diff --git a/src/uu/pinky/src/platform/openbsd.rs b/src/uu/pinky/src/platform/openbsd.rs new file mode 100644 index 000000000..7e6970c1f --- /dev/null +++ b/src/uu/pinky/src/platform/openbsd.rs @@ -0,0 +1,17 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// Specific implementation for OpenBSD: tool unsupported (utmpx not supported) + +use crate::uu_app; + +use uucore::error::UResult; + +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let _matches = uu_app().try_get_matches_from(args)?; + + println!("unsupported command on OpenBSD"); + Ok(()) +} diff --git a/src/uu/pinky/src/platform/unix.rs b/src/uu/pinky/src/platform/unix.rs new file mode 100644 index 000000000..381acacd2 --- /dev/null +++ b/src/uu/pinky/src/platform/unix.rs @@ -0,0 +1,293 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore (ToDO) BUFSIZE gecos fullname, mesg iobuf + +use crate::options; +use crate::uu_app; +use crate::Capitalize; + +use uucore::entries::{Locate, Passwd}; +use uucore::error::{FromIo, UResult}; +use uucore::libc::S_IWGRP; +use uucore::utmpx::{self, time, Utmpx}; + +use std::io::prelude::*; +use std::io::BufReader; + +use std::fs::File; +use std::os::unix::fs::MetadataExt; + +use std::path::PathBuf; + +fn get_long_usage() -> String { + format!( + "A lightweight 'finger' program; print user information.\n\ + The utmp file will be {}.", + utmpx::DEFAULT_FILE + ) +} + +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = args.collect_ignore(); + + let matches = uu_app() + .after_help(get_long_usage()) + .try_get_matches_from(args)?; + + let users: Vec = matches + .get_many::(options::USER) + .map(|v| v.map(ToString::to_string).collect()) + .unwrap_or_default(); + + // If true, display the hours:minutes since each user has touched + // the keyboard, or blank if within the last minute, or days followed + // by a 'd' if not within the last day. + let mut include_idle = true; + + // If true, display a line at the top describing each field. + let include_heading = !matches.get_flag(options::OMIT_HEADINGS); + + // if true, display the user's full name from pw_gecos. + let mut include_fullname = true; + + // if true, display the user's ~/.project file when doing long format. + let include_project = !matches.get_flag(options::OMIT_PROJECT_FILE); + + // if true, display the user's ~/.plan file when doing long format. + let include_plan = !matches.get_flag(options::OMIT_PLAN_FILE); + + // if true, display the user's home directory and shell + // when doing long format. + let include_home_and_shell = !matches.get_flag(options::OMIT_HOME_DIR); + + // if true, use the "short" output format. + let do_short_format = !matches.get_flag(options::LONG_FORMAT); + + /* if true, display the ut_host field. */ + let mut include_where = true; + + if matches.get_flag(options::OMIT_NAME) { + include_fullname = false; + } + if matches.get_flag(options::OMIT_NAME_HOST) { + include_fullname = false; + include_where = false; + } + if matches.get_flag(options::OMIT_NAME_HOST_TIME) { + include_fullname = false; + include_idle = false; + include_where = false; + } + + let pk = Pinky { + include_idle, + include_heading, + include_fullname, + include_project, + include_plan, + include_home_and_shell, + include_where, + names: users, + }; + + if do_short_format { + match pk.short_pinky() { + Ok(_) => Ok(()), + Err(e) => Err(e.map_err_context(String::new)), + } + } else { + pk.long_pinky(); + Ok(()) + } +} + +struct Pinky { + include_idle: bool, + include_heading: bool, + include_fullname: bool, + include_project: bool, + include_plan: bool, + include_where: bool, + include_home_and_shell: bool, + names: Vec, +} + +fn idle_string(when: i64) -> String { + thread_local! { + static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap(); + } + NOW.with(|n| { + let duration = n.unix_timestamp() - when; + if duration < 60 { + // less than 1min + " ".to_owned() + } else if duration < 24 * 3600 { + // less than 1day + let hours = duration / (60 * 60); + let minutes = (duration % (60 * 60)) / 60; + format!("{hours:02}:{minutes:02}") + } else { + // more than 1day + let days = duration / (24 * 3600); + format!("{days}d") + } + }) +} + +fn time_string(ut: &Utmpx) -> String { + // "%b %e %H:%M" + let time_format: Vec = + time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]") + .unwrap(); + ut.login_time().format(&time_format).unwrap() // LC_ALL=C +} + +fn gecos_to_fullname(pw: &Passwd) -> Option { + let mut gecos = if let Some(gecos) = &pw.user_info { + gecos.clone() + } else { + return None; + }; + if let Some(n) = gecos.find(',') { + gecos.truncate(n); + } + Some(gecos.replace('&', &pw.name.capitalize())) +} + +impl Pinky { + fn print_entry(&self, ut: &Utmpx) -> std::io::Result<()> { + let mut pts_path = PathBuf::from("/dev"); + pts_path.push(ut.tty_device().as_str()); + + let mesg; + let last_change; + + match pts_path.metadata() { + #[allow(clippy::unnecessary_cast)] + Ok(meta) => { + mesg = if meta.mode() & S_IWGRP as u32 == 0 { + '*' + } else { + ' ' + }; + last_change = meta.atime(); + } + _ => { + mesg = '?'; + last_change = 0; + } + } + + print!("{1:<8.0$}", utmpx::UT_NAMESIZE, ut.user()); + + if self.include_fullname { + let fullname = if let Ok(pw) = Passwd::locate(ut.user().as_ref()) { + gecos_to_fullname(&pw) + } else { + None + }; + if let Some(fullname) = fullname { + print!(" {fullname:<19.19}"); + } else { + print!(" {:19}", " ???"); + } + } + + print!(" {}{:<8.*}", mesg, utmpx::UT_LINESIZE, ut.tty_device()); + + if self.include_idle { + if last_change == 0 { + print!(" {:<6}", "?????"); + } else { + print!(" {:<6}", idle_string(last_change)); + } + } + + print!(" {}", time_string(ut)); + + let mut s = ut.host(); + if self.include_where && !s.is_empty() { + s = ut.canon_host()?; + print!(" {s}"); + } + + println!(); + Ok(()) + } + + fn print_heading(&self) { + print!("{:<8}", "Login"); + if self.include_fullname { + print!(" {:<19}", "Name"); + } + print!(" {:<9}", " TTY"); + if self.include_idle { + print!(" {:<6}", "Idle"); + } + print!(" {:<16}", "When"); + if self.include_where { + print!(" Where"); + } + println!(); + } + + fn short_pinky(&self) -> std::io::Result<()> { + if self.include_heading { + self.print_heading(); + } + for ut in Utmpx::iter_all_records() { + if ut.is_user_process() + && (self.names.is_empty() || self.names.iter().any(|n| n.as_str() == ut.user())) + { + self.print_entry(&ut)?; + } + } + Ok(()) + } + + fn long_pinky(&self) { + for u in &self.names { + print!("Login name: {u:<28}In real life: "); + if let Ok(pw) = Passwd::locate(u.as_str()) { + let fullname = gecos_to_fullname(&pw).unwrap_or_default(); + let user_dir = pw.user_dir.unwrap_or_default(); + let user_shell = pw.user_shell.unwrap_or_default(); + println!(" {fullname}"); + if self.include_home_and_shell { + print!("Directory: {user_dir:<29}"); + println!("Shell: {user_shell}"); + } + if self.include_project { + let mut p = PathBuf::from(&user_dir); + p.push(".project"); + if let Ok(f) = File::open(p) { + print!("Project: "); + read_to_console(f); + } + } + if self.include_plan { + let mut p = PathBuf::from(&user_dir); + p.push(".plan"); + if let Ok(f) = File::open(p) { + println!("Plan:"); + read_to_console(f); + } + } + println!(); + } else { + println!(" ???"); + } + } + } +} + +fn read_to_console(f: F) { + let mut reader = BufReader::new(f); + let mut iobuf = Vec::new(); + if reader.read_to_end(&mut iobuf).is_ok() { + print!("{}", String::from_utf8_lossy(&iobuf)); + } +}