ps: Add command ps (#133)

* ps: Add command `ps`

* pidof: Add some basic arguments and flags

* ps: Introduce new dependencies.

Introduce `uu_pgrep` for processing processes, and introduce `prettytable-rs` for output.

* ps: Finish command infrastructure.

* pgrep: Implemented `Hash` trait for `ProcessInformation`.

* misc: Introduce `nix` crate

* pgrep: Rename `TerminalType` to `Teletype`

* ps: Use `Vec<Rc<RefCell<_>>>` for mutable data.

* misc: Fix conflict

* misc: Fix fmt

* pgrep: Revert refactoring.

* ps: Add `basic_collector`

* ps: Introduce `picker` mod to do data picking.

* ps: Implemented user defined data picking

* ps: Collect and apply header mapping and use `Vec` instead of `LinkedList`

* ps: Add license header

* ps: Apply suggestions

* ps: Implemented logic of `-o`

* ps: Implement basic function.

* ps: Remove empty line of output

* ps: Implemented nullable format parser.

And also introduce `thiserror` crate.

* ps: Implemented `-o` (`--format`)

* ps: Add license header for `parser.rs`

* misc: Apply modifications from upstream

* ps: Implemented code of `time`

* ps: Fix check failure

* ps: Fix build failure on Windows

* ps: Enable tests for another *NIX system.

* ps: Revert tests support for *NIX platform.

It looks like it's very difficult to implement the ps command on *NIX platforms as portable, as it's only portable if you have a proc pseudo-fs. In other words, the current design only even supports Linux.

- FreeBSD's proc pseudo-fs is not automounted.
- macOS does not have a proc fs, it use sysctl instead.

* ps: Refactor mappings to `mapping.rs`

* ps: Clean code

* misc: Bump version of `nix` crate.

* ps: Rewrite document and optimize code.

* ps: Clean TODO and add TODO

Remove TODO in `mapping.rs` and add TODO in `collector.rs`

* ps: Allow multiple occurrences of the same code.

* ps: Fix default codes' display

* ps: Use `Result` instead of `Option` in `parser.rs`

* ps: Remove duplicate elements of `proc_infos`

* ps: Implemented sorting

But only implemented the basic usage: sorting by pid.

* ps: Rename functions in `sorting.rs`

- Rename `sorting` to `sort`
- Rename `default_sort` to `sort_by_pid`

---------

Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
Krysztal Huang
2024-08-07 16:04:00 +08:00
committed by GitHub
parent 906d893113
commit e039c3e451
13 changed files with 1004 additions and 3 deletions
Generated
+178 -2
View File
@@ -122,6 +122,12 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.38"
@@ -218,6 +224,27 @@ version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "deranged"
version = "0.3.11"
@@ -233,6 +260,27 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "dirs-next"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"dirs-sys-next",
]
[[package]]
name = "dirs-sys-next"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "dns-lookup"
version = "2.0.4"
@@ -251,6 +299,12 @@ version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]]
name = "errno"
version = "0.3.8"
@@ -324,6 +378,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "is-terminal"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
]
[[package]]
name = "itoa"
version = "1.0.11"
@@ -339,12 +404,28 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.5.0",
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
@@ -377,7 +458,19 @@ checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"cfg_aliases",
"cfg_aliases 0.1.1",
"libc",
]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
]
@@ -495,6 +588,20 @@ dependencies = [
"yansi",
]
[[package]]
name = "prettytable-rs"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eea25e07510aa6ab6547308ebe3c036016d162b8da920dbb079e3ba8acf3d95a"
dependencies = [
"csv",
"encode_unicode",
"is-terminal",
"lazy_static",
"term",
"unicode-width",
]
[[package]]
name = "proc-macro2"
version = "1.0.79"
@@ -526,6 +633,7 @@ dependencies = [
"uu_pgrep",
"uu_pidof",
"uu_pmap",
"uu_ps",
"uu_pwdx",
"uu_slabtop",
"uu_w",
@@ -593,6 +701,17 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "redox_users"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.6"
@@ -664,6 +783,18 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -759,6 +890,17 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]]
name = "terminal_size"
version = "0.2.6"
@@ -791,6 +933,26 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.36"
@@ -885,6 +1047,20 @@ dependencies = [
"uucore",
]
[[package]]
name = "uu_ps"
version = "0.0.1"
dependencies = [
"chrono",
"clap",
"libc",
"nix 0.29.0",
"prettytable-rs",
"thiserror",
"uu_pgrep",
"uucore",
]
[[package]]
name = "uu_pwdx"
version = "0.0.1"
@@ -929,7 +1105,7 @@ dependencies = [
"dns-lookup",
"glob",
"libc",
"nix",
"nix 0.28.0",
"number_prefix",
"once_cell",
"os_display",
+15 -1
View File
@@ -25,7 +25,17 @@ build = "build.rs"
default = ["feat_common_core"]
uudoc = []
feat_common_core = ["pwdx", "free", "w", "watch", "pmap", "slabtop", "pgrep", "pidof"]
feat_common_core = [
"pwdx",
"free",
"w",
"watch",
"pmap",
"slabtop",
"pgrep",
"pidof",
"ps",
]
[workspace.dependencies]
uucore = "0.0.27"
@@ -38,12 +48,15 @@ libc = "0.2.154"
phf = "0.11.2"
phf_codegen = "0.11.2"
textwrap = { version = "0.16.1", features = ["terminal_size"] }
thiserror = "1.0.63"
xattr = "1.3.1"
tempfile = "3.10.1"
rand = { version = "0.8.5", features = ["small_rng"] }
bytesize = "1.3.0"
chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
walkdir = "2.5.0"
prettytable-rs = "0.10.0"
nix = { version = "0.29", default-features = false }
[dependencies]
clap = { workspace = true }
@@ -64,6 +77,7 @@ pmap = { optional = true, version = "0.0.1", package = "uu_pmap", path = "src/uu
slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" }
pgrep = { optional = true, version = "0.0.1", package = "uu_pgrep", path = "src/uu/pgrep" }
pidof = { optional = true, version = "0.0.1", package = "uu_pidof", path = "src/uu/pidof" }
ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps" }
[dev-dependencies]
pretty_assertions = "1.4.0"
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "uu_ps"
version = "0.0.1"
edition = "2021"
authors = ["uutils developers"]
license = "MIT"
description = "ps - (uutils) Report a snapshot of the current processes"
homepage = "https://github.com/uutils/procps"
repository = "https://github.com/uutils/procps/tree/main/src/uu/ps"
keywords = ["acl", "uutils", "cross-platform", "cli", "utility"]
categories = ["command-line-utilities"]
[dependencies]
uucore = { workspace = true, features = ["utmpx"] }
clap = { workspace = true }
chrono = { workspace = true, default-features = false, features = ["clock"] }
libc = { workspace = true }
prettytable-rs = { workspace = true }
nix = { workspace = true }
thiserror = { workspace = true }
uu_pgrep = { path = "../pgrep" }
[lib]
path = "src/ps.rs"
[[bin]]
name = "ps"
path = "src/main.rs"
+7
View File
@@ -0,0 +1,7 @@
# ps
```
ps [options]
```
Report a snapshot of the current processes.
+109
View File
@@ -0,0 +1,109 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use clap::ArgMatches;
#[cfg(target_family = "unix")]
use nix::errno::Errno;
use std::{cell::RefCell, path::PathBuf, rc::Rc, str::FromStr};
use uu_pgrep::process::{ProcessInformation, Teletype};
// TODO: Temporary add to this file, this function will add to uucore.
#[cfg(not(target_os = "redox"))]
#[cfg(target_family = "unix")]
fn getsid(pid: i32) -> Option<i32> {
unsafe {
let result = libc::getsid(pid);
if Errno::last() == Errno::UnknownErrno {
Some(result)
} else {
None
}
}
}
// TODO: Temporary add to this file, this function will add to uucore.
#[cfg(target_family = "windows")]
fn getsid(_pid: i32) -> Option<i32> {
Some(0)
}
// Guessing it matches the current terminal
pub(crate) fn basic_collector(
proc_snapshot: &[Rc<RefCell<ProcessInformation>>],
) -> Vec<Rc<RefCell<ProcessInformation>>> {
let mut result = Vec::new();
let current_tty = {
// SAFETY: The `libc::getpid` always return i32
let proc_path =
PathBuf::from_str(&format!("/proc/{}/", unsafe { libc::getpid() })).unwrap();
let current_proc_info = ProcessInformation::try_new(proc_path).unwrap();
current_proc_info.tty()
};
for proc_info in proc_snapshot {
let proc_ttys = proc_info.borrow().tty();
if proc_ttys == current_tty {
result.push(proc_info.clone())
}
}
result
}
/// Filter for processes
///
/// - `-A` Select all processes. Identical to `-e`.
pub(crate) fn process_collector(
matches: &ArgMatches,
proc_snapshot: &[Rc<RefCell<ProcessInformation>>],
) -> Vec<Rc<RefCell<ProcessInformation>>> {
let mut result = Vec::new();
// flag `-A`
if matches.get_flag("A") {
result.extend(proc_snapshot.iter().map(Rc::clone))
}
result
}
/// Filter for session
///
/// - `-d` Select all processes except session leaders.
/// - `-a` Select all processes except both session leaders (see getsid(2)) and processes not associated with a terminal.
pub(crate) fn session_collector(
matches: &ArgMatches,
proc_snapshot: &[Rc<RefCell<ProcessInformation>>],
) -> Vec<Rc<RefCell<ProcessInformation>>> {
let mut result = Vec::new();
let tty = |proc: &Rc<RefCell<ProcessInformation>>| proc.borrow_mut().tty();
// flag `-d`
// TODO: Implementation this collection, guessing it pid=sid
if matches.get_flag("d") {
proc_snapshot.iter().for_each(|_| {});
}
// flag `-a`
// Guessing it pid=sid, and associated terminal.
if matches.get_flag("a") {
proc_snapshot.iter().for_each(|it| {
let pid = it.borrow().pid;
if let Some(sid) = getsid(pid as i32) {
// Check is session leader
if sid != (pid as i32) && tty(it) != Teletype::Unknown {
result.push(it.clone())
}
}
});
}
result
}
+1
View File
@@ -0,0 +1 @@
uucore::bin!(uu_ps);
+180
View File
@@ -0,0 +1,180 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::parser::OptionalKeyValue;
use std::collections::HashMap;
pub(crate) fn collect_code_mapping(formats: &[OptionalKeyValue]) -> Vec<(String, String)> {
let mapping = default_mapping();
formats
.iter()
.map(|it| {
let key = it.key().to_string();
match it.value() {
Some(value) => (key, value.clone()),
None => (key.clone(), mapping.get(&key).unwrap().to_string()),
}
})
.collect()
}
/// Returns the default codes.
pub(crate) fn default_codes() -> Vec<String> {
["pid", "tname", "time", "ucmd"].map(Into::into).to_vec()
}
/// Collect mapping from argument
pub(crate) fn default_mapping() -> HashMap<String, String> {
let mut mapping = HashMap::new();
let mut append = |code: &str, header: &str| mapping.insert(code.into(), header.into());
// Those mapping generated from manpage
append("%cpu", "%CPU");
append("%mem", "%MEM");
append("ag_id", "AGID");
append("ag_nice", "AGNI");
append("args", "COMMAND");
append("blocked", "BLOCKED");
append("bsdstart", "START");
append("bsdtime", "TIME");
append("c", "C");
append("caught", "CAUGHT");
append("cgname", "CGNAME");
append("cgroup", "CGROUP");
append("cgroupns", "CGROUPNS");
append("class", "CLS");
append("cls", "CLS");
append("cmd", "CMD");
append("comm", "COMMAND");
append("command", "COMMAND");
append("cp", "CP");
append("cputime", "TIME");
append("cputimes", "TIME");
append("cuc", "%CUC");
append("cuu", "%CUU");
append("drs", "DRS");
append("egid", "EGID");
append("egroup", "EGROUP");
append("eip", "EIP");
append("esp", "ESP");
append("etime", "ELAPSED");
append("etimes", "ELAPSED");
append("euid", "EUID");
append("euser", "EUSER");
append("exe", "EXE");
append("f", "F");
append("fgid", "FGID");
append("fgroup", "FGROUP");
append("flag", "F");
append("flags", "F");
append("fname", "COMMAND");
append("fuid", "FUID");
append("fuser", "FUSER");
append("gid", "GID");
append("group", "GROUP");
append("ignored", "IGNORED");
append("ipcns", "IPCNS");
append("label", "LABEL");
append("lstart", "STARTED");
append("lsession", "SESSION");
append("luid", "LUID");
append("lwp", "LWP");
append("lxc", "LXC");
append("machine", "MACHINE");
append("maj_flt", "MAJFLT");
append("min_flt", "MINFLT");
append("mntns", "MNTNS");
append("netns", "NETNS");
append("ni", "NI");
append("nice", "NI");
append("nlwp", "NLWP");
append("numa", "NUMA");
append("nwchan", "WCHAN");
append("oom", "OOM");
append("oomadj", "OOMADJ");
append("ouid", "OWNER");
append("pcpu", "%CPU");
append("pending", "PENDING");
append("pgid", "PGID");
append("pgrp", "PGRP");
append("pid", "PID");
append("pidns", "PIDNS");
append("pmem", "%MEM");
append("policy", "POL");
append("ppid", "PPID");
append("pri", "PRI");
append("psr", "PSR");
append("pss", "PSS");
append("rbytes", "RBYTES");
append("rchars", "RCHARS");
append("rgid", "RGID");
append("rgroup", "RGROUP");
append("rops", "ROPS");
append("rss", "RSS");
append("rssize", "RSS");
append("rsz", "RSZ");
append("rtprio", "RTPRIO");
append("ruid", "RUID");
append("ruser", "RUSER");
append("s", "S");
append("sched", "SCH");
append("seat", "SEAT");
append("sess", "SESS");
append("sgi_p", "P");
append("sgid", "SGID");
append("sgroup", "SGROUP");
append("sid", "SID");
append("sig", "PENDING");
append("sigcatch", "CAUGHT");
append("sigignore", "IGNORED");
append("sigmask", "BLOCKED");
append("size", "SIZE");
append("slice", "SLICE");
append("spid", "SPID");
append("stackp", "STACKP");
append("start", "STARTED");
append("start_time", "START");
append("stat", "STAT");
append("state", "S");
append("stime", "STIME");
append("suid", "SUID");
append("supgid", "SUPGID");
append("supgrp", "SUPGRP");
append("suser", "SUSER");
append("svgid", "SVGID");
append("svuid", "SVUID");
append("sz", "SZ");
append("tgid", "TGID");
append("thcount", "THCNT");
append("tid", "TID");
append("time", "TIME");
append("timens", "TIMENS");
append("times", "TIME");
append("tname", "TTY");
append("tpgid", "TPGID");
append("trs", "TRS");
append("tt", "TT");
append("tty", "TT");
append("ucmd", "CMD");
append("ucomm", "COMMAND");
append("uid", "UID");
append("uname", "USER");
append("unit", "UNIT");
append("user", "USER");
append("userns", "USERNS");
append("uss", "USS");
append("utsns", "UTSNS");
append("uunit", "UUNIT");
append("vsize", "VSZ");
append("vsz", "VSZ");
append("wbytes", "WBYTES");
append("wcbytes", "WCBYTES");
append("wchan", "WCHAN");
append("wchars", "WCHARS");
append("wops", "WOPS");
mapping
}
+136
View File
@@ -0,0 +1,136 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::convert::Infallible;
use thiserror::Error as TError;
#[derive(Debug, TError, PartialEq, Eq)]
pub enum Error {
#[error("empty value")]
EmptyValue,
#[error("parsing failed")]
ParsingFailed,
}
/// Parsing _**optional**_ key-value arguments
///
/// There are two formats
///
/// - `cmd` -> key: `cmd`, value: None
/// - `cmd=CMD` -> key: `cmd`, value: `CMD`
///
/// Other formats can also be parsed:
///
/// - `cmd=` -> key: `cmd`, value: (empty, no space there)
/// - `cmd=abcd123~~~~` -> key: `cmd`, value: `abcd123~~~~`
/// - `cmd======?` -> key: `cmd`, value: `=====?`
#[derive(Debug, Clone)]
pub struct OptionalKeyValue {
key: String,
value: Option<String>,
}
impl OptionalKeyValue {
pub fn new<T>(value: T) -> Self
where
T: Into<String>,
{
let value: String = value.into();
if let Some((key, value)) = value.split_once("=") {
Self {
key: key.into(),
value: Some(value.into()),
}
} else {
Self {
key: value,
value: None,
}
}
}
pub fn with_key<T>(key: T) -> Self
where
T: Into<String>,
{
Self {
key: key.into(),
value: None,
}
}
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> &Option<String> {
&self.value
}
pub fn is_value_empty(&self) -> bool {
self.value.is_none()
}
pub fn try_get<T: std::str::FromStr>(&self) -> Result<T, Error> {
let Some(ref value) = self.value else {
return Err(Error::EmptyValue);
};
value.parse::<T>().map_err(|_| Error::ParsingFailed)
}
}
// clap value parser wrapper
pub(crate) fn parser(value: &str) -> Result<OptionalKeyValue, Infallible> {
Ok(OptionalKeyValue::new(value))
}
#[cfg(test)]
mod tests {
use super::*;
#[inline(always)]
fn new<T>(value: T) -> OptionalKeyValue
where
T: Into<String>,
{
OptionalKeyValue::new(value)
}
#[test]
fn test_parsing() {
assert!(new("value").is_value_empty());
assert!(!new("value=").is_value_empty());
assert!(!new("value=v").is_value_empty());
assert!(!new("value=?:").is_value_empty());
assert!(OptionalKeyValue::with_key("key").key().eq("key"));
}
#[test]
fn test_get_key() {
assert_eq!(new("value").key(), "value");
assert_eq!(new("value=").key(), "value");
assert_eq!(new("value=?").key(), "value");
}
#[test]
fn test_get_value() {
// String test
assert_eq!(new("value").try_get::<String>(), Err(Error::EmptyValue));
assert_eq!(new("value=").try_get::<String>(), Ok("".into()));
assert_eq!(new("value=?").try_get::<String>(), Ok("?".into()));
// Number test
assert_eq!(new("value").try_get::<usize>(), Err(Error::EmptyValue));
assert_eq!(new("value=0").try_get::<usize>(), Ok(0));
assert_eq!(new("value=0").try_get::<i128>(), Ok(0));
assert_eq!(new("value=-1").try_get::<i128>(), Ok(-1));
assert_eq!(new("value=0").try_get::<u128>(), Ok(0));
assert_eq!(new("value=-1").try_get::<u128>(), Err(Error::ParsingFailed));
}
}
+72
View File
@@ -0,0 +1,72 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use std::cell::RefCell;
use chrono::DateTime;
use uu_pgrep::process::{ProcessInformation, Teletype};
pub(crate) fn collect_pickers(
code_order: &[String],
) -> Vec<Box<dyn Fn(RefCell<ProcessInformation>) -> String>> {
let mut pickers = Vec::new();
for code in code_order {
match code.as_str() {
"pid" | "tgid" => pickers.push(helper(pid)),
"tname" | "tt" | "tty" => pickers.push(helper(tty)),
"time" | "cputime" => pickers.push(helper(time)),
"ucmd" => pickers.push(helper(ucmd)),
"cmd" => pickers.push(helper(cmd)),
_ => {}
}
}
pickers
}
#[inline]
fn helper(
f: impl Fn(RefCell<ProcessInformation>) -> String + 'static,
) -> Box<dyn Fn(RefCell<ProcessInformation>) -> String> {
Box::new(f)
}
fn pid(proc_info: RefCell<ProcessInformation>) -> String {
format!("{}", proc_info.borrow().pid)
}
fn tty(proc_info: RefCell<ProcessInformation>) -> String {
match proc_info.borrow().tty() {
Teletype::Tty(tty) => format!("tty{}", tty),
Teletype::TtyS(ttys) => format!("ttyS{}", ttys),
Teletype::Pts(pts) => format!("pts/{}", pts),
Teletype::Unknown => "?".to_owned(),
}
}
fn time(proc_info: RefCell<ProcessInformation>) -> String {
// https://docs.kernel.org/filesystems/proc.html#id10
// Index of 13 14
let cumulative_cpu_time = {
let utime = proc_info.borrow_mut().stat()[13].parse::<i64>().unwrap();
let stime = proc_info.borrow_mut().stat()[14].parse::<i64>().unwrap();
utime + stime
};
DateTime::from_timestamp_millis(cumulative_cpu_time)
.unwrap()
.format("%H:%M:%S")
.to_string()
}
fn cmd(proc_info: RefCell<ProcessInformation>) -> String {
proc_info.borrow().cmdline.clone()
}
fn ucmd(proc_info: RefCell<ProcessInformation>) -> String {
proc_info.borrow_mut().status().get("Name").unwrap().into()
}
+206
View File
@@ -0,0 +1,206 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
mod collector;
mod mapping;
mod parser;
mod picker;
mod sorting;
use clap::crate_version;
use clap::{Arg, ArgAction, ArgMatches, Command};
use mapping::{collect_code_mapping, default_codes, default_mapping};
use parser::{parser, OptionalKeyValue};
use prettytable::{format::consts::FORMAT_CLEAN, Row, Table};
use std::{cell::RefCell, rc::Rc};
use uu_pgrep::process::walk_process;
use uucore::{
error::{UError, UResult, USimpleError},
format_usage, help_about, help_usage,
};
const ABOUT: &str = help_about!("ps.md");
const USAGE: &str = help_usage!("ps.md");
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let snapshot = walk_process()
.map(|it| Rc::new(RefCell::new(it)))
.collect::<Vec<_>>();
let mut proc_infos = Vec::new();
proc_infos.extend(collector::basic_collector(&snapshot));
proc_infos.extend(collector::process_collector(&matches, &snapshot));
proc_infos.extend(collector::session_collector(&matches, &snapshot));
proc_infos.dedup_by(|a, b| a.borrow().pid == b.borrow().pid);
sorting::sort(&mut proc_infos, &matches);
let arg_formats = collect_format(&matches);
let Ok(arg_formats) = arg_formats else {
return Err(arg_formats.err().unwrap());
};
// Collect codes with order
let codes = if arg_formats.is_empty() {
default_codes()
} else {
arg_formats.iter().map(|it| it.key().to_owned()).collect()
};
// Collect pickers ordered by codes
let pickers = picker::collect_pickers(&codes);
// Constructing table
let mut rows = Vec::new();
for proc in proc_infos {
let picked = pickers
.iter()
.map(|picker| picker(Rc::unwrap_or_clone(proc.clone())));
rows.push(Row::from_iter(picked));
}
// Apply header mapping
let code_mapping = if arg_formats.is_empty() {
let default_mapping = default_mapping();
default_codes();
codes
.into_iter()
.map(|code| (code.clone(), default_mapping[&code].to_string()))
.collect::<Vec<_>>()
} else {
collect_code_mapping(&arg_formats)
};
let header = code_mapping
.iter()
.map(|(_, header)| header)
.map(Into::into)
.collect::<Vec<String>>();
// Apply header
let mut table = Table::from_iter([Row::from_iter(header)]);
table.set_format(*FORMAT_CLEAN);
table.extend(rows);
print!("{}", table);
Ok(())
}
fn collect_format(
matches: &ArgMatches,
) -> Result<Vec<OptionalKeyValue>, Box<dyn UError + 'static>> {
let arg_format = matches.get_many::<OptionalKeyValue>("format");
let collect = arg_format.unwrap_or_default().cloned().collect::<Vec<_>>();
let default_mapping = default_mapping();
// Validate key is exist
for key in collect.iter().map(OptionalKeyValue::key) {
if !default_mapping.contains_key(key) {
return Err(USimpleError::new(
1,
format!("error: unknown user-defined format specifier \"{key}\""),
));
}
}
Ok(collect)
}
#[allow(clippy::cognitive_complexity)]
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.disable_help_flag(true)
.arg(Arg::new("help").long("help").action(ArgAction::Help))
.args([
Arg::new("A")
.short('A')
.help("all processes")
.visible_alias("e")
.action(ArgAction::SetTrue),
Arg::new("a")
.short('a')
.help("all with tty, except session leaders")
.action(ArgAction::SetTrue),
// Arg::new("a_")
// .short('a')
// .help("all with tty, including other users")
// .action(ArgAction::SetTrue)
// .allow_hyphen_values(true),
Arg::new("d")
.short('d')
.help("all except session leaders")
.action(ArgAction::SetTrue),
Arg::new("deselect")
.long("deselect")
.short('N')
.help("negate selection")
.action(ArgAction::SetTrue),
// Arg::new("r")
// .short('r')
// .action(ArgAction::SetTrue)
// .help("only running processes")
// .allow_hyphen_values(true),
// Arg::new("T")
// .short('T')
// .action(ArgAction::SetTrue)
// .help("all processes on this terminal")
// .allow_hyphen_values(true),
// Arg::new("x")
// .short('x')
// .action(ArgAction::SetTrue)
// .help("processes without controlling ttys")
// .allow_hyphen_values(true),
])
.arg(
Arg::new("format")
.short('o')
.long("format")
.action(ArgAction::Append)
.value_delimiter(',')
.value_parser(parser)
.help("user-defined format"),
)
// .args([
// Arg::new("command").short('c').help("command name"),
// Arg::new("GID")
// .short('G')
// .long("Group")
// .help("real group id or name"),
// Arg::new("group")
// .short('g')
// .long("group")
// .help("session or effective group name"),
// Arg::new("PID").short('p').long("pid").help("process id"),
// Arg::new("pPID").long("ppid").help("parent process id"),
// Arg::new("qPID")
// .short('q')
// .long("quick-pid")
// .help("process id"),
// Arg::new("session")
// .short('s')
// .long("sid")
// .help("session id"),
// Arg::new("t").short('t').long("tty").help("terminal"),
// Arg::new("eUID")
// .short('u')
// .long("user")
// .help("effective user id or name"),
// Arg::new("rUID")
// .short('U')
// .long("User")
// .help("real user id or name"),
// ])
}
+18
View File
@@ -0,0 +1,18 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use clap::ArgMatches;
use std::{cell::RefCell, rc::Rc};
use uu_pgrep::process::ProcessInformation;
// TODO: Implementing sorting flags.
pub(crate) fn sort(input: &mut [Rc<RefCell<ProcessInformation>>], _matches: &ArgMatches) {
sort_by_pid(input)
}
/// Sort by pid. (Default)
fn sort_by_pid(input: &mut [Rc<RefCell<ProcessInformation>>]) {
input.sort_by(|a, b| a.borrow().pid.cmp(&b.borrow().pid))
}
+48
View File
@@ -0,0 +1,48 @@
// This file is part of the uutils procps package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::common::util::TestScenario;
#[test]
fn test_invalid_arg() {
new_ucmd!().arg("--definitely-invalid").fails().code_is(1);
}
#[test]
#[cfg(target_os = "linux")]
fn test_code_mapping() {
new_ucmd!()
.args(&["-o", "cmd=CCMD"])
.succeeds()
.stdout_contains("CCMD");
new_ucmd!().args(&["-o", "cmd= "]).succeeds();
new_ucmd!().args(&["-o", "ccmd=CCMD"]).fails().code_is(1);
new_ucmd!()
.args(&["-o", "cmd=CMD1", "-o", "cmd=CMD2"])
.succeeds()
.stdout_contains("CMD1")
.stdout_contains("CMD2");
new_ucmd!()
.args(&["-o", "cmd=CMD1,cmd=CMD2"])
.succeeds()
.stdout_contains("CMD1")
.stdout_contains("CMD2");
new_ucmd!()
.args(&["-o", "ucmd=CMD1", "-o", "ucmd=CMD2"])
.succeeds()
.stdout_contains("CMD1")
.stdout_contains("CMD2");
new_ucmd!()
.args(&["-o", "ucmd=CMD1,ucmd=CMD2"])
.succeeds()
.stdout_contains("CMD1")
.stdout_contains("CMD2");
}
+4
View File
@@ -36,3 +36,7 @@ mod test_pgrep;
#[cfg(feature = "pidof")]
#[path = "by-util/test_pidof.rs"]
mod test_pidof;
#[cfg(feature = "ps")]
#[path = "by-util/test_ps.rs"]
mod test_ps;