diff --git a/Cargo.toml b/Cargo.toml index 20c086236..b9132bf73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ unix = [ "nice", "nohup", "pathchk", + "pinky", "stat", "stdbuf", "timeout", @@ -145,6 +146,7 @@ nproc = { optional=true, path="src/nproc" } od = { optional=true, path="src/od" } paste = { optional=true, path="src/paste" } pathchk = { optional=true, path="src/pathchk" } +pinky = { optional=true, path="src/pinky" } printenv = { optional=true, path="src/printenv" } printf = { optional=true, path="src/printf" } ptx = { optional=true, path="src/ptx" } diff --git a/Makefile b/Makefile index ef15f05ad..600c56a5a 100644 --- a/Makefile +++ b/Makefile @@ -116,6 +116,7 @@ UNIX_PROGS := \ nice \ nohup \ pathchk \ + pinky \ stat \ stdbuf \ timeout \ @@ -164,6 +165,7 @@ TEST_PROGS := \ od \ paste \ pathchk \ + pinky \ printf \ ptx \ pwd \ diff --git a/README.md b/README.md index f4d0801b5..e8b85269b 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,6 @@ To do - chcon - chgrp -- chown - copy - cp (not much done) - csplit @@ -163,7 +162,6 @@ To do - mv (almost done, one more option) - numfmt - od (in progress, needs lots of work) -- pinky - pr - printf - remove diff --git a/src/pinky/Cargo.toml b/src/pinky/Cargo.toml new file mode 100644 index 000000000..0e83fbf4a --- /dev/null +++ b/src/pinky/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pinky" +version = "0.0.1" +authors = [] + +[lib] +name = "uu_pinky" +path = "pinky.rs" + +[dependencies] +getopts = "*" +time = "*" +libc = "^0.2" +uucore = { path="../uucore" } + +[[bin]] +name = "pinky" +path = "main.rs" diff --git a/src/pinky/main.rs b/src/pinky/main.rs new file mode 100644 index 000000000..57068d670 --- /dev/null +++ b/src/pinky/main.rs @@ -0,0 +1,5 @@ +extern crate uu_pinky; + +fn main() { + std::process::exit(uu_pinky::uumain(std::env::args().collect())); +} diff --git a/src/pinky/pinky.rs b/src/pinky/pinky.rs new file mode 100644 index 000000000..cec6022a3 --- /dev/null +++ b/src/pinky/pinky.rs @@ -0,0 +1,477 @@ +#![crate_name = "uu_pinky"] + +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// +#![cfg_attr(feature="clippy", feature(plugin))] +#![cfg_attr(feature="clippy", plugin(clippy))] + +#[macro_use] +extern crate uucore; +use uucore::c_types::getpwnam; +use uucore::utmpx; + +extern crate getopts; +extern crate libc; +use libc::{uid_t, gid_t, c_char, S_IWGRP}; + +extern crate time; + +use std::io::prelude::*; +use std::io::BufReader; +use std::io::Result as IOResult; + +use std::fs::File; +use std::os::unix::fs::MetadataExt; + +use std::ptr; +use std::ffi::{CStr, CString, OsStr}; +use std::os::unix::ffi::OsStrExt; + +use std::path::Path; +use std::path::PathBuf; + +mod utmp; + +static NAME: &'static str = "pinky"; +static VERSION: &'static str = env!("CARGO_PKG_VERSION"); + +const BUFSIZE: usize = 1024; + +pub fn uumain(args: Vec) -> i32 { + let mut opts = getopts::Options::new(); + opts.optflag("l", + "l", + "produce long format output for the specified USERs"); + opts.optflag("b", + "b", + "omit the user's home directory and shell in long format"); + opts.optflag("h", "h", "omit the user's project file in long format"); + opts.optflag("p", "p", "omit the user's plan file in long format"); + opts.optflag("s", "s", "do short format output, this is the default"); + opts.optflag("f", "f", "omit the line of column headings in short format"); + opts.optflag("w", "w", "omit the user's full name in short format"); + opts.optflag("i", + "i", + "omit the user's full name and remote host in short format"); + opts.optflag("q", + "q", + "omit the user's full name, remote host and idle time in short format"); + opts.optflag("", "help", "display this help and exit"); + opts.optflag("", "version", "output version information and exit"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(f) => { + disp_err!("{}", f); + return 1; + } + }; + + if matches.opt_present("help") { + println!("Usage: {} [OPTION]... [USER]... + + -l produce long format output for the specified USERs + -b omit the user's home directory and shell in long format + -h omit the user's project file in long format + -p omit the user's plan file in long format + -s do short format output, this is the default + -f omit the line of column headings in short format + -w omit the user's full name in short format + -i omit the user's full name and remote host in short format + -q omit the user's full name, remote host and idle time + in short format + --help display this help and exit + --version output version information and exit + +A lightweight 'finger' program; print user information. +The utmp file will be {}", + NAME, + utmpx::DEFAULT_FILE); + return 0; + } + + if matches.opt_present("version") { + println!("{} {}", NAME, VERSION); + return 0; + } + + // 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.opt_present("f"); + + // 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.opt_present("h"); + + // if true, display the user's ~/.plan file when doing long format. + let include_plan = !matches.opt_present("p"); + + // if true, display the user's home directory and shell + // when doing long format. + let include_home_and_shell = !matches.opt_present("b"); + + // if true, use the "short" output format. + let do_short_format = !matches.opt_present("l"); + + /* if true, display the ut_host field. */ + let mut include_where = true; + + if matches.opt_present("w") { + include_fullname = false; + } + if matches.opt_present("i") { + include_fullname = false; + include_where = false; + } + if matches.opt_present("q") { + include_fullname = false; + include_idle = false; + include_where = false; + } + + if !do_short_format && matches.free.is_empty() { + disp_err!("no username specified; at least one must be specified when using -l"); + return 1; + } + + let pk = Pinky { + include_idle: include_idle, + include_heading: include_heading, + include_fullname: include_fullname, + include_project: include_project, + include_plan: include_plan, + include_home_and_shell: include_home_and_shell, + include_where: include_where, + names: matches.free, + }; + + if do_short_format { + if let Err(e) = pk.short_pinky() { + disp_err!("{}", e); + 1 + } else { + 0 + } + } else { + pk.long_pinky() + } + +} + +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, +} + +#[derive(Debug)] +pub struct Passwd { + pw_name: String, + pw_passwd: String, + pw_uid: uid_t, + pw_gid: gid_t, + pw_gecos: String, + pw_dir: String, + pw_shell: String, +} + +trait FromChars { + fn from_chars(*const c_char) -> Self; +} + +impl FromChars for String { + #[inline] + fn from_chars(ptr: *const c_char) -> Self { + if ptr.is_null() { + return "".to_owned(); + } + let s = unsafe { CStr::from_ptr(ptr) }; + s.to_string_lossy().into_owned() + } +} + +pub fn getpw(u: &str) -> Option { + let pw = unsafe { + getpwnam(CString::new(u).unwrap().as_ptr()) + }; + if !pw.is_null() { + let data = unsafe { ptr::read(pw) }; + Some(Passwd { + pw_name: String::from_chars(data.pw_name), + pw_passwd: String::from_chars(data.pw_passwd), + pw_uid: data.pw_uid, + pw_gid: data.pw_gid, + pw_dir: String::from_chars(data.pw_dir), + pw_gecos: String::from_chars(data.pw_gecos), + pw_shell: String::from_chars(data.pw_shell), + }) + } else { + None + } +} + +pub trait Capitalize { + fn capitalize(&self) -> String; +} + +impl Capitalize for str { + fn capitalize(&self) -> String { + use std::ascii::AsciiExt; + self.char_indices().fold(String::with_capacity(self.len()), |mut acc, x| { + if x.0 != 0 { + acc.push(x.1) + } else { + acc.push(x.1.to_ascii_uppercase()) + } + acc + }) + } +} + +trait UtmpUtils { + fn is_user_process(&self) -> bool; +} + +impl UtmpUtils for utmpx::c_utmp { + fn is_user_process(&self) -> bool { + self.ut_user[0] != 0 && self.ut_type == utmpx::USER_PROCESS + } +} + +fn idle_string(when: i64) -> String { + thread_local! { + static NOW: time::Tm = time::now() + } + NOW.with(|n| { + let duration = n.to_timespec().sec - 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!("{:02}:{:02}", hours, minutes) + } else { + // more than 1day + let days = duration / (24 * 3600); + format!("{}d", days) + } + }) +} + +fn time_string(ut: &utmpx::c_utmp) -> String { + let tm = time::at(time::Timespec::new(ut.ut_tv.tv_sec as i64, ut.ut_tv.tv_usec as i32)); + time::strftime("%Y-%m-%d %H:%M", &tm).unwrap() +} + +const AI_CANONNAME: libc::c_int = 0x2; + +fn canon_host(host: &str) -> Option { + let hints = libc::addrinfo { + ai_flags: AI_CANONNAME, + ai_family: 0, + ai_socktype: 0, + ai_protocol: 0, + ai_addrlen: 0, + ai_addr: ptr::null_mut(), + ai_canonname: ptr::null_mut(), + ai_next: ptr::null_mut(), + }; + let c_host = CString::new(host).unwrap(); + let mut res = ptr::null_mut(); + let status = unsafe { + libc::getaddrinfo(c_host.as_ptr(), ptr::null(), &hints as *const _, &mut res as *mut _) + }; + if status == 0 { + let info: libc::addrinfo = unsafe { + ptr::read(res as *const _) + }; + // http://lists.gnu.org/archive/html/bug-coreutils/2006-09/msg00300.html + // says Darwin 7.9.0 getaddrinfo returns 0 but sets + // res->ai_canonname to NULL. + let ret = if info.ai_canonname.is_null() { + Some(String::from(host)) + } else { + Some(unsafe { + CString::from_raw(info.ai_canonname).into_string().unwrap() + }) + }; + unsafe { + libc::freeaddrinfo(res); + } + ret + } else { + None + } +} + +impl Pinky { + fn print_entry(&self, ut: &utmpx::c_utmp) { + let mut pts_path = PathBuf::from("/dev"); + let line: &Path = OsStr::from_bytes(unsafe { + CStr::from_ptr(ut.ut_line.as_ref().as_ptr()).to_bytes() + }).as_ref(); + pts_path.push(line); + + let mesg; + let last_change; + match pts_path.metadata() { + Ok(meta) => { + mesg = if meta.mode() & (S_IWGRP as u32) != 0 { + ' ' + } else { + '*' + }; + last_change = meta.atime(); + } + _ => { + mesg = '?'; + last_change = 0; + } + } + + let ut_user = String::from_chars(ut.ut_user.as_ref().as_ptr()); + print!("{1:<8.0$}", utmpx::UT_NAMESIZE, ut_user); + + if self.include_fullname { + if let Some(pw) = getpw(&ut_user) { + let mut gecos = pw.pw_gecos; + if let Some(n) = gecos.find(',') { + gecos.truncate(n + 1); + } + print!(" {:<19.19}", gecos.replace("&", &pw.pw_name.capitalize())); + } else { + print!(" {:19}", " ???"); + } + + } + + print!(" {}{:<8.*}", mesg, utmpx::UT_LINESIZE, String::from_chars(ut.ut_line.as_ref().as_ptr())); + + if self.include_idle { + if last_change != 0 { + print!(" {:<6}", idle_string(last_change)); + } else { + print!(" {:<6}", "?????"); + } + } + + // WARNING: Because of the definition of `struct utmp`, + // pinky cannot get the correct value of utmp.ut_tv + print!(" {}", time_string(&ut)); + + if self.include_where && ut.ut_host[0] != 0 { + let ut_host = String::from_chars(ut.ut_host.as_ref().as_ptr()); + let mut res = ut_host.split(':'); + let host = match res.next() { + Some(h) => canon_host(&h).unwrap_or(ut_host.clone()), + None => ut_host.clone(), + }; + match res.next() { + Some(d) => print!(" {}:{}", host, d), + None => print!(" {}", host), + } + } + + println!(""); + } + + 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) -> IOResult<()> { + if self.include_heading { + self.print_heading(); + } + for ut in utmp::read_utmps() { + if ut.is_user_process() { + if self.names.is_empty() { + self.print_entry(&ut) + } else { + let ut_user = unsafe { + CStr::from_ptr(ut.ut_user.as_ref().as_ptr()).to_bytes() + }; + if self.names.iter().any(|n| n.as_bytes() == ut_user) { + self.print_entry(&ut); + } + } + } + } + Ok(()) + } + + fn long_pinky(&self) -> i32 { + for u in &self.names { + print!("Login name: {:<28}In real life: ", u); + if let Some(pw) = getpw(u) { + println!(" {}", pw.pw_gecos.replace("&", &pw.pw_name.capitalize())); + if self.include_home_and_shell { + print!("Directory: {:<29}", pw.pw_dir); + println!("Shell: {}", pw.pw_shell); + } + if self.include_project { + let mut p = PathBuf::from(&pw.pw_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(&pw.pw_dir); + p.push(".plan"); + if let Ok(f) = File::open(p) { + println!("Plan:"); + read_to_console(f); + } + } + println!(""); + } else { + println!(" ???"); + } + } + 0 + } +} + +fn read_to_console(f: F) { + let mut reader = BufReader::new(f); + let mut iobuf = [0_u8; BUFSIZE]; + while let Ok(n) = reader.read(&mut iobuf) { + if n == 0 { + break; + } + let s = String::from_utf8_lossy(&iobuf); + print!("{}", s); + } +} diff --git a/src/pinky/utmp.rs b/src/pinky/utmp.rs new file mode 100644 index 000000000..93f32bfff --- /dev/null +++ b/src/pinky/utmp.rs @@ -0,0 +1,50 @@ +// This file is part of the uutils coreutils package. +// +// (c) Jian Zeng +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// +extern crate uucore; +use uucore::utmpx::c_utmp; + +use std::ptr; + +#[cfg(unix)] +extern "C" { + fn getutxent() -> *const c_utmp; + fn setutxent(); + fn endutxent(); +} + +pub struct UtmpIter; + +impl UtmpIter { + fn new() -> Self { + unsafe { + setutxent(); + } + UtmpIter + } +} + +impl Iterator for UtmpIter { + type Item = c_utmp; + + fn next(&mut self) -> Option { + unsafe { + let line = getutxent(); + + if line.is_null() { + endutxent(); + return None; + } + + Some(ptr::read(line)) + } + } +} + +pub fn read_utmps() -> UtmpIter { + UtmpIter::new() +} diff --git a/src/uucore/c_types.rs b/src/uucore/c_types.rs index 477927ba0..89169979b 100644 --- a/src/uucore/c_types.rs +++ b/src/uucore/c_types.rs @@ -8,6 +8,8 @@ use self::libc::{ uid_t, gid_t, }; +pub use self::libc::passwd as c_passwd; + #[cfg(any(target_os = "macos", target_os = "freebsd"))] use self::libc::time_t; #[cfg(target_os = "macos")] @@ -22,35 +24,6 @@ use std::vec::Vec; use std::ptr::{null_mut, read}; -#[cfg(any(target_os = "macos", target_os = "freebsd"))] -#[repr(C)] -#[derive(Clone, Copy)] -pub struct c_passwd { - pub pw_name: *const c_char, /* user name */ - pub pw_passwd: *const c_char, /* user name */ - pub pw_uid: uid_t, /* user uid */ - pub pw_gid: gid_t, /* user gid */ - pub pw_change: time_t, - pub pw_class: *const c_char, - pub pw_gecos: *const c_char, - pub pw_dir: *const c_char, - pub pw_shell: *const c_char, - pub pw_expire: time_t -} - -#[cfg(target_os = "linux")] -#[repr(C)] -#[derive(Clone, Copy)] -pub struct c_passwd { - pub pw_name: *const c_char, /* user name */ - pub pw_passwd: *const c_char, /* user name */ - pub pw_uid: uid_t, /* user uid */ - pub pw_gid: gid_t, /* user gid */ - pub pw_gecos: *const c_char, - pub pw_dir: *const c_char, - pub pw_shell: *const c_char, -} - #[cfg(any(target_os = "macos", target_os = "freebsd"))] #[repr(C)] pub struct utsname { diff --git a/src/uucore/utmpx.rs b/src/uucore/utmpx.rs index 58bedc91a..8b6bc742e 100644 --- a/src/uucore/utmpx.rs +++ b/src/uucore/utmpx.rs @@ -2,7 +2,7 @@ extern crate libc; -pub use self::utmpx::{DEFAULT_FILE,USER_PROCESS,BOOT_TIME,c_utmp}; +pub use self::utmpx::{UT_NAMESIZE,UT_LINESIZE,DEFAULT_FILE,USER_PROCESS,BOOT_TIME,c_utmp}; #[cfg(target_os = "linux")] mod utmpx { use super::libc; diff --git a/tests/test_pinky.rs b/tests/test_pinky.rs new file mode 100644 index 000000000..35af9c777 --- /dev/null +++ b/tests/test_pinky.rs @@ -0,0 +1,80 @@ +use common::util::*; + +static UTIL_NAME: &'static str = "pinky"; + +extern crate uu_pinky; +pub use self::uu_pinky::*; + +#[test] +fn test_capitalize() { + assert_eq!("Zbnmasd", "zbnmasd".capitalize()); + assert_eq!("Abnmasd", "Abnmasd".capitalize()); + assert_eq!("1masd", "1masd".capitalize()); + assert_eq!("", "".capitalize()); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_long_format() { + let (_, mut ucmd) = testing(UTIL_NAME); + ucmd.arg("-l").arg("root"); + let expected = "Login name: root In real life: root\nDirectory: /root Shell: /bin/bash\n\n"; + assert_eq!(expected, ucmd.run().stdout); + + let (_, mut ucmd) = testing(UTIL_NAME); + ucmd.arg("-lb").arg("root"); + let expected = "Login name: root In real life: root\n\n"; + assert_eq!(expected, ucmd.run().stdout); +} + +#[test] +#[cfg(target_os = "macos")] +fn test_long_format() { + let (_, mut ucmd) = testing(UTIL_NAME); + ucmd.arg("-l").arg("root"); + let expected = "Login name: root In real life: System Administrator\nDirectory: /var/root Shell: /bin/sh\n\n"; + assert_eq!(expected, ucmd.run().stdout); + + let (_, mut ucmd) = testing(UTIL_NAME); + ucmd.arg("-lb").arg("root"); + let expected = "Login name: root In real life: System Administrator\n\n"; + assert_eq!(expected, ucmd.run().stdout); +} + +#[cfg(target_os = "linux")] +#[test] +#[ignore] +fn test_short_format() { + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-s"]; + ucmd.args(&args); + assert_eq!(expected_result(&args), ucmd.run().stdout); + + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-f"]; + ucmd.args(&args); + assert_eq!(expected_result(&args), ucmd.run().stdout); + + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-w"]; + ucmd.args(&args); + assert_eq!(expected_result(&args), ucmd.run().stdout); + + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-i"]; + ucmd.args(&args); + assert_eq!(expected_result(&args), ucmd.run().stdout); + + let (_, mut ucmd) = testing(UTIL_NAME); + let args = ["-q"]; + ucmd.args(&args); + assert_eq!(expected_result(&args), ucmd.run().stdout); +} + +#[cfg(target_os = "linux")] +fn expected_result(args: &[&str]) -> String { + use std::process::Command; + + let output = Command::new(UTIL_NAME).args(args).output().unwrap(); + String::from_utf8_lossy(&output.stdout).into_owned() +} diff --git a/tests/test_stat.rs b/tests/test_stat.rs index 37dfe68c9..13e93d14d 100644 --- a/tests/test_stat.rs +++ b/tests/test_stat.rs @@ -145,8 +145,11 @@ fn test_invalid_option() { ucmd.fails(); } +#[allow(unused_variable)] const NORMAL_FMTSTR: &'static str = "%a %A %b %B %d %D %f %F %g %G %h %i %m %n %o %s %u %U %w %W %x %X %y %Y %z %Z"; +#[allow(unused_variable)] const DEV_FMTSTR: &'static str = "%a %A %b %B %d %D %f %F %g %G %h %i %m %n %o %s (%t/%T) %u %U %w %W %x %X %y %Y %z %Z"; +#[allow(unused_variable)] const FS_FMTSTR: &'static str = "%a %b %c %d %f %i %l %n %s %S %t %T"; #[test] @@ -230,6 +233,7 @@ fn test_printf() { assert_eq!(ucmd.run().stdout, "123?\r\"\\\x07\x08\x1B\x0C\x0B /\x12wZJ\n"); } +#[allow(dead_code)] fn expected_result(args: &[&str]) -> String { use std::process::Command; diff --git a/tests/tests.rs b/tests/tests.rs index 4a88f2d91..40059f299 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -30,6 +30,7 @@ unix_only! { "install", test_install; "mv", test_mv; "pathchk", test_pathchk; + "pinky", test_pinky; "stdbuf", test_stdbuf; "touch", test_touch; "unlink", test_unlink;