1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-09-17 20:26:18 +00:00

pinky: tool unsupported on OpenBSD

- utmpx not supported on OpenBSD

  - add src/uu/pinky/src/platform directory and platform/mod.rs for conditional compilation
    according to target_os
  - platform/openbsd.rs: implementation on OpenBSD (unsupported tool)
  - platform/unix.rs: implementation on other OS
  - src/uu/pinky/src/pinky.rs: use platform module for uucore::main function
This commit is contained in:
Laurent Cheylus 2023-12-09 18:31:58 +01:00
parent e6d12732f5
commit d3e6e7a947
4 changed files with 327 additions and 281 deletions

View file

@ -5,21 +5,11 @@
// spell-checker:ignore (ToDO) BUFSIZE gecos fullname, mesg iobuf // 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 clap::{crate_version, Arg, ArgAction, Command};
use std::path::PathBuf;
use uucore::{format_usage, help_about, help_usage}; use uucore::{format_usage, help_about, help_usage};
mod platform;
const ABOUT: &str = help_about!("pinky.md"); const ABOUT: &str = help_about!("pinky.md");
const USAGE: &str = help_usage!("pinky.md"); const USAGE: &str = help_usage!("pinky.md");
@ -37,88 +27,8 @@ mod options {
pub const HELP: &str = "help"; 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] #[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> { use platform::uumain;
let args = args.collect_ignore();
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let users: Vec<String> = matches
.get_many::<String>(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(())
}
}
pub fn uu_app() -> Command { pub fn uu_app() -> Command {
Command::new(uucore::util_name()) 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<String>,
}
pub trait Capitalize { pub trait Capitalize {
fn capitalize(&self) -> String; 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::FormatItem> =
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<String> {
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: Read>(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));
}
}

View file

@ -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::*;

View file

@ -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(())
}

View file

@ -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<String> = matches
.get_many::<String>(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<String>,
}
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::FormatItem> =
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<String> {
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: Read>(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));
}
}