From 49fc7ff3ca36bcb89f754ffefc10b277755b3e74 Mon Sep 17 00:00:00 2001 From: Andy <137199874+Puffy1215@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:43:18 +0800 Subject: [PATCH] Add last Utility (#65) --- Cargo.lock | 289 +++++++++++---- Cargo.toml | 4 + src/uu/last/Cargo.toml | 12 + src/uu/last/last.md | 8 + src/uu/last/src/last.rs | 94 +++++ src/uu/last/src/main.rs | 1 + src/uu/last/src/platform/mod.rs | 19 + src/uu/last/src/platform/openbsd.rs | 17 + src/uu/last/src/platform/unix.rs | 535 ++++++++++++++++++++++++++++ src/uu/last/src/platform/windows.rs | 17 + tests/by-util/test_last.rs | 111 ++++++ tests/tests.rs | 4 + 12 files changed, 1034 insertions(+), 77 deletions(-) create mode 100644 src/uu/last/Cargo.toml create mode 100644 src/uu/last/last.md create mode 100644 src/uu/last/src/last.rs create mode 100644 src/uu/last/src/main.rs create mode 100644 src/uu/last/src/platform/mod.rs create mode 100644 src/uu/last/src/platform/openbsd.rs create mode 100644 src/uu/last/src/platform/unix.rs create mode 100644 src/uu/last/src/platform/windows.rs create mode 100644 tests/by-util/test_last.rs diff --git a/Cargo.lock b/Cargo.lock index 340f741..2f3cd34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,24 +4,25 @@ version = 3 [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" -version = "0.6.7" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd2405b3ac1faab2990b74d728624cd9fd115651fcecc7c2d8daf01376275ba" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] @@ -33,27 +34,27 @@ checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -67,9 +68,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bytecount" @@ -77,6 +78,12 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cfg-if" version = "1.0.0" @@ -122,9 +129,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "clap_mangen" @@ -138,15 +145,15 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-deque" @@ -169,9 +176,18 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] [[package]] name = "diff" @@ -179,17 +195,29 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "dns-lookup" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc" +dependencies = [ + "cfg-if", + "libc", + "socket2", + "windows-sys 0.48.0", +] + [[package]] name = "either" -version = "1.9.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -197,9 +225,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fnv" @@ -209,9 +237,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -232,9 +260,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -253,6 +281,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itoa" version = "1.0.11" @@ -261,9 +295,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -279,15 +313,15 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "nix" @@ -295,7 +329,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "cfg-if", "cfg_aliases", "libc", @@ -310,6 +344,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -380,11 +429,20 @@ dependencies = [ "siphasher", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "pretty_assertions" @@ -422,9 +480,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -435,11 +493,11 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "hex", "lazy_static", "procfs-core", - "rustix 0.38.34", + "rustix 0.38.37", ] [[package]] @@ -448,15 +506,15 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "hex", ] [[package]] name = "quote" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -493,9 +551,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -503,9 +561,9 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ "crossbeam-deque", "crossbeam-utils", @@ -525,9 +583,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -536,9 +594,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "rlimit" @@ -551,9 +609,9 @@ dependencies = [ [[package]] name = "roff" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" +checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" [[package]] name = "rustix" @@ -571,14 +629,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys 0.4.12", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] @@ -605,7 +663,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.77", ] [[package]] @@ -632,6 +690,16 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "strsim" version = "0.11.1" @@ -651,9 +719,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.58" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -706,7 +774,7 @@ dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix 0.38.34", + "rustix 0.38.37", "windows-sys 0.59.0", ] @@ -726,7 +794,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix 0.38.34", + "rustix 0.38.37", "windows-sys 0.48.0", ] @@ -742,11 +810,44 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-linebreak" @@ -762,9 +863,9 @@ checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util-linux" @@ -773,6 +874,7 @@ dependencies = [ "clap", "clap_complete", "clap_mangen", + "dns-lookup", "libc", "phf", "phf_codegen", @@ -784,6 +886,7 @@ dependencies = [ "tempfile", "textwrap", "uu_ctrlaltdel", + "uu_last", "uu_lscpu", "uu_lsmem", "uu_mountpoint", @@ -800,6 +903,15 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_last" +version = "0.0.1" +dependencies = [ + "clap", + "dns-lookup", + "uucore", +] + [[package]] name = "uu_lscpu" version = "0.0.1" @@ -844,21 +956,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b54aad02cf7e96f5fafabb6b836efa73eef934783b17530095a29ffd4fdc154" dependencies = [ "clap", + "dns-lookup", "glob", "libc", "nix", "number_prefix", "once_cell", "os_display", + "time", "uucore_procs", "wild", ] [[package]] name = "uucore_procs" -version = "0.0.24" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb9aeeb06d1f15c5b3b51acddddf3436e3e1480902b2a200618ca5dbb24e392" +checksum = "c3d588f57acb2ba416e072a6fa652f2e11cf727267c697d2e2d65175f3b10c41" dependencies = [ "proc-macro2", "quote", @@ -867,9 +981,9 @@ dependencies = [ [[package]] name = "uuhelp_parser" -version = "0.0.24" +version = "0.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d841f8408028085ca65896cdd60b9925d4e407cb69989a64889f2bebbb51147b" +checksum = "96f26868814bf1ca9deec910a08007c93eb1d8e407ce36451999d4c1c1ea6767" [[package]] name = "version_check" @@ -944,7 +1058,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.77", ] [[package]] @@ -955,7 +1069,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.58", + "syn 2.0.77", ] [[package]] @@ -1122,8 +1236,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", - "linux-raw-sys 0.4.12", - "rustix 0.38.34", + "linux-raw-sys 0.4.14", + "rustix 0.38.37", ] [[package]] @@ -1131,3 +1245,24 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] diff --git a/Cargo.toml b/Cargo.toml index 984008e..e13e459 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ feat_common_core = [ "lsmem", "ctrlaltdel", "rev", + "last" ] [workspace.dependencies] @@ -50,6 +51,7 @@ rand = { version = "0.8", features = ["small_rng"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.122" tabled = "0.16.0" +dns-lookup = "2.0.4" [dependencies] clap = { workspace = true } @@ -58,6 +60,7 @@ clap_mangen = { workspace = true } uucore = { workspace = true } phf = { workspace = true } textwrap = { workspace = true } +dns-lookup = { workspace = true } # lscpu = { optional = true, version = "0.0.1", package = "uu_lscpu", path = "src/uu/lscpu" } @@ -65,6 +68,7 @@ lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/ mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" } ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } +last = { optional = true, version = "0.0.1", package = "uu_last", path = "src/uu/last" } [dev-dependencies] pretty_assertions = "1" diff --git a/src/uu/last/Cargo.toml b/src/uu/last/Cargo.toml new file mode 100644 index 0000000..077080b --- /dev/null +++ b/src/uu/last/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "uu_last" +version = "0.0.1" +edition = "2021" + +[lib] +path = "src/last.rs" + +[dependencies] +uucore = { workspace = true, features = ["utmpx"] } +clap = { workspace = true} +dns-lookup = { workspace = true } diff --git a/src/uu/last/last.md b/src/uu/last/last.md new file mode 100644 index 0000000..be9919e --- /dev/null +++ b/src/uu/last/last.md @@ -0,0 +1,8 @@ +# last + +``` +Usage: + [options] [...] [...] +``` + +Show a listing of last logged in users. diff --git a/src/uu/last/src/last.rs b/src/uu/last/src/last.rs new file mode 100644 index 0000000..bb77bde --- /dev/null +++ b/src/uu/last/src/last.rs @@ -0,0 +1,94 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, Arg, ArgAction, Command}; +use uucore::{format_usage, help_about, help_usage}; + +mod platform; + +mod options { + pub const SYSTEM: &str = "system"; + pub const HOSTLAST: &str = "hostlast"; + pub const NO_HOST: &str = "nohostname"; + pub const LIMIT: &str = "limit"; + pub const DNS: &str = "dns"; + pub const TIME_FORMAT: &str = "time-format"; + pub const USER_TTY: &str = "username"; + pub const FILE: &str = "file"; +} + +const ABOUT: &str = help_about!("last.md"); +const USAGE: &str = help_usage!("last.md"); + +#[uucore::main] +use platform::uumain; + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new(options::FILE) + .short('f') + .long("file") + .action(ArgAction::Set) + .default_value("/var/log/wtmp") + .help("use a specific file instead of /var/log/wtmp") + .required(false), + ) + .arg( + Arg::new(options::SYSTEM) + .short('x') + .long(options::SYSTEM) + .action(ArgAction::SetTrue) + .required(false) + .help("display system shutdown entries and run level changes"), + ) + .arg( + Arg::new(options::DNS) + .short('d') + .long(options::DNS) + .action(ArgAction::SetTrue) + .required(false) + .help("translate the IP number back into a hostname"), + ) + .arg( + Arg::new(options::HOSTLAST) + .short('a') + .long(options::HOSTLAST) + .action(ArgAction::SetTrue) + .required(false) + .help("display hostnames in the last column"), + ) + .arg( + Arg::new(options::NO_HOST) + .short('R') + .long(options::NO_HOST) + .action(ArgAction::SetTrue) + .required(false) + .help("don't display the hostname field"), + ) + .arg( + Arg::new(options::LIMIT) + .short('n') + .long(options::LIMIT) + .action(ArgAction::Set) + .required(false) + .help("how many lines to show") + .value_parser(clap::value_parser!(i32)) + .allow_negative_numbers(true), + ) + .arg( + Arg::new(options::TIME_FORMAT) + .long(options::TIME_FORMAT) + .action(ArgAction::Set) + .required(false) + .help("show timestamps in the specified : notime|short|full|iso") + .default_value("short"), + ) + .arg(Arg::new(options::USER_TTY).action(ArgAction::Append)) +} diff --git a/src/uu/last/src/main.rs b/src/uu/last/src/main.rs new file mode 100644 index 0000000..0a7dcca --- /dev/null +++ b/src/uu/last/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_last); diff --git a/src/uu/last/src/platform/mod.rs b/src/uu/last/src/platform/mod.rs new file mode 100644 index 0000000..b3b1aa6 --- /dev/null +++ b/src/uu/last/src/platform/mod.rs @@ -0,0 +1,19 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use self::unix::*; + +#[cfg(target_os = "openbsd")] +mod openbsd; +#[cfg(target_os = "openbsd")] +pub use self::openbsd::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use self::windows::*; diff --git a/src/uu/last/src/platform/openbsd.rs b/src/uu/last/src/platform/openbsd.rs new file mode 100644 index 0000000..8a0613b --- /dev/null +++ b/src/uu/last/src/platform/openbsd.rs @@ -0,0 +1,17 @@ +// This file is part of the uutils util-linux 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/last/src/platform/unix.rs b/src/uu/last/src/platform/unix.rs new file mode 100644 index 0000000..d8f3f2c --- /dev/null +++ b/src/uu/last/src/platform/unix.rs @@ -0,0 +1,535 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::options; +use crate::uu_app; + +use uucore::error::UIoError; +use uucore::error::UResult; + +use uucore::error::USimpleError; +use uucore::utmpx::time::OffsetDateTime; +use uucore::utmpx::{time, Utmpx}; + +use std::fmt::Write; +use std::fs; +use std::io; +use std::net::Ipv4Addr; + +use std::os::unix::fs::MetadataExt; +use std::path::PathBuf; +use std::str::FromStr; +use std::time::Duration; + +fn get_long_usage() -> String { + format!( + "If FILE is not specified, use {}. /var/log/wtmp as FILE is common.", + WTMP_PATH, + ) +} + +const WTMP_PATH: &str = "/var/log/wtmp"; +static TIME_FORMAT_STR: [&str; 4] = ["notime", "short", "full", "iso"]; + +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app() + .after_help(get_long_usage()) + .try_get_matches_from(args)?; + + let system = matches.get_flag(options::SYSTEM); + let dns = matches.get_flag(options::DNS); + let hostlast = matches.get_flag(options::HOSTLAST); + let nohost = matches.get_flag(options::NO_HOST); + let limit: i32 = if let Some(num) = matches.get_one::(options::LIMIT) { + *num + } else { + 0 // Original implementation has 0 mean no limit (print all values) + }; + + let time_format = if let Some(format) = matches.get_one::(options::TIME_FORMAT) { + let format_str = format.as_str().trim(); + if TIME_FORMAT_STR.contains(&format_str) { + Ok(format.to_string()) + } else { + Err(USimpleError::new( + 0, + format!("unknown time format: {format}"), + )) + } + } else { + Ok("short".to_string()) + }?; + + let file: String = if let Some(files) = matches.get_one::(options::FILE) { + files.to_string() + } else { + WTMP_PATH.to_string() + }; + + let user: Option> = + if let Some(users) = matches.get_many::(options::USER_TTY) { + users + .map(|v| { + if is_numeric(v) { + Some(format!("tty{v}")) + } else { + Some(v.to_owned()) + } + }) + .collect() + } else { + None + }; + + let mut last = Last { + last_reboot_ut: None, + last_shutdown_ut: None, + last_dead_ut: vec![], + system, + dns, + host_last: hostlast, + no_host: nohost, + limit, + file: file.to_string(), + users: user, + time_format, + }; + + last.exec() +} + +const RUN_LEVEL_STR: &str = "runlevel"; +const REBOOT_STR: &str = "reboot"; +const SHUTDOWN_STR: &str = "shutdown"; + +struct Last { + last_reboot_ut: Option, + last_shutdown_ut: Option, + last_dead_ut: Vec, + system: bool, + dns: bool, + host_last: bool, + no_host: bool, + file: String, + time_format: String, + users: Option>, + limit: i32, +} + +fn is_numeric(s: &str) -> bool { + s.chars().all(|c| c.is_numeric()) +} + +#[inline] +fn calculate_time_delta( + curr_datetime: &OffsetDateTime, + last_datetime: &OffsetDateTime, +) -> time::Duration { + let curr_duration = time::Duration::new( + curr_datetime.unix_timestamp(), + curr_datetime.nanosecond().try_into().unwrap_or_default(), // nanosecond value is always a value between 0 and 1.000.000.000, shouldn't panic + ); + + let last_duration = time::Duration::new( + last_datetime.unix_timestamp(), + last_datetime.nanosecond().try_into().unwrap_or_default(), // nanosecond value is always a value between 0 and 1.000.000.000, shouldn't panic + ); + + last_duration - curr_duration +} + +#[inline] +fn duration_string(duration: time::Duration) -> String { + let mut seconds = duration.whole_seconds(); + + let days = seconds / 86400; + seconds -= days * 86400; + let hours = seconds / 3600; + seconds -= hours * 3600; + let minutes = seconds / 60; + + if days > 0 { + format!("({}+{:0>2}:{:0>2})", days, hours, minutes) + } else { + format!("({:0>2}:{:0>2})", hours, minutes) + } +} + +fn find_dns_name(ut: &Utmpx) -> String { + let default = Ipv4Addr::new(0, 0, 0, 0); + let ip = std::net::IpAddr::V4(Ipv4Addr::from_str(&ut.host()).unwrap_or(default)); + + if ip.to_string().trim() == "0.0.0.0" { + ip.to_string() + } else { + dns_lookup::lookup_addr(&ip).unwrap_or_default() + } +} + +impl Last { + const TIME_FULL_FMT: &'static str = "[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year]"; + const END_TIME_SHORT_FMT: &'static str = "[hour]:[minute]"; + const START_TIME_SHORT_FMT: &'static str = + "[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]"; + const TIME_ISO_FMT: &'static str = + "[year]-[month]-[day]T[hour]:[minute]:[second]+[offset_hour]:[offset_minute]"; + + #[allow(clippy::cognitive_complexity)] + fn exec(&mut self) -> UResult<()> { + let mut ut_stack: Vec = vec![]; + // For 'last' output, older output needs to be printed last (FILO), as + // UtmpxIter does not implement Rev trait. A better implementation + // might include implementing UtmpxIter as doubly linked + Utmpx::iter_all_records_from(&self.file).for_each(|ut| ut_stack.push(ut)); + + let mut counter = 0; + let mut first_ut_time = None; + while let Some(ut) = ut_stack.pop() { + if ut_stack.is_empty() { + // By the end of loop we will have the earliest time + // (This avoids getting into issues with the compiler) + let first_login_time = ut.login_time(); + first_ut_time = Some(self.utmp_file_time( + first_login_time.unix_timestamp(), + first_login_time.nanosecond().into(), + )); + } + + if counter >= self.limit && self.limit > 0 { + break; + } + if ut.is_user_process() { + let mut dead_proc: Option = None; + if let Some(pos) = self + .last_dead_ut + .iter() + .position(|dead_ut| ut.tty_device() == dead_ut.tty_device()) + { + dead_proc = Some(self.last_dead_ut.swap_remove(pos)); + } + if self.print_user(&ut, dead_proc.as_ref()) { + counter += 1; + } + } else if ut.user() == RUN_LEVEL_STR { + if self.print_runlevel(&ut) { + counter += 1; + } + } else if ut.user() == SHUTDOWN_STR { + if self.print_shutdown(&ut) { + counter += 1; + } + self.last_shutdown_ut = Some(ut); + } else if ut.user() == REBOOT_STR { + if self.print_reboot(&ut) { + counter += 1; + } + self.last_reboot_ut = Some(ut); + } else if ut.user() == "" { + // Dead process end date + self.last_dead_ut.push(ut); + } + } + + let path = std::path::absolute(&self.file)?; + let path_str = path + .file_name() + .ok_or_else(|| { + if path.is_dir() { + UIoError::new(io::ErrorKind::InvalidData, "Is a directory") + } else { + UIoError::new(io::ErrorKind::Unsupported, "Undefined") + } + })? + .to_str() + .ok_or(UIoError::new( + io::ErrorKind::InvalidData, + "invalid character data (not UTF-8)", + ))?; + + if let Some(file_time) = first_ut_time { + println!("\n{} begins {}", path_str, file_time); + } else { + let secs = fs::metadata(&self.file)?.ctime(); + let nsecs = fs::metadata(&self.file)?.ctime_nsec() as u64; + let file_time = self.utmp_file_time(secs, nsecs); + + println!("\n{} begins {}", path_str, file_time); + } + + Ok(()) + } + + #[inline] + fn utmp_file_time(&self, secs: i64, nsecs: u64) -> String { + let description = match self.time_format.as_str() { + "short" | "full" => Self::TIME_FULL_FMT, + "iso" => Self::TIME_ISO_FMT, + _ => return "".to_string(), + }; + + let time_format: Vec = + time::format_description::parse(description).unwrap_or_default(); + + let time = time::OffsetDateTime::from_unix_timestamp(secs) + .unwrap_or(time::OffsetDateTime::UNIX_EPOCH) + + Duration::from_nanos(nsecs); + + let offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC); + let offset_secs: u64 = offset.whole_seconds() as u64; + + // Adding back the time to the offset so that offset_time is correct. + let offset_time = time.replace_offset(offset) + Duration::from_secs(offset_secs); + + offset_time.format(&time_format).unwrap_or_default() + } + + #[inline] + fn time_string(&self, ut: &Utmpx) -> String { + let description = match self.time_format.as_str() { + "short" => Self::START_TIME_SHORT_FMT, + "full" => Self::TIME_FULL_FMT, + "iso" => Self::TIME_ISO_FMT, + _ => return "".to_string(), + }; + + // "%b %e %H:%M" + let time_format: Vec = + time::format_description::parse(description).unwrap_or_default(); + ut.login_time().format(&time_format).unwrap_or_default() + } + + #[inline] + fn end_time_string(&self, user_process_str: Option<&str>, end_ut: &OffsetDateTime) -> String { + match user_process_str { + Some(val) => val.to_string(), + _ => { + let description = match self.time_format.as_str() { + "short" => format!("- {}", Self::END_TIME_SHORT_FMT), + "full" => format!("- {}", Self::TIME_FULL_FMT), + "iso" => format!("- {}", Self::TIME_ISO_FMT), + _ => return "".to_string(), + }; + + // "%H:%M" + let time_format: Vec = + time::format_description::parse(&description).unwrap_or_default(); + end_ut.format(&time_format).unwrap_or_default() + } + } + } + + #[inline] + fn end_state_string(&self, ut: &Utmpx, dead_ut: Option<&Utmpx>) -> (String, String) { + // This function takes a considerable amount of CPU cycles to complete; + // root cause seems to be the ut.login_time function, which reads a + // file to determine local offset for UTC. Perhaps this function + // should be updated to save that UTC offset for subsequent calls + let mut proc_status: Option<&str> = None; + let curr_datetime = ut.login_time(); + + if let Some(dead) = dead_ut { + let dead_datetime = dead.login_time(); + let time_delta = duration_string(calculate_time_delta(&curr_datetime, &dead_datetime)); + return ( + self.end_time_string(proc_status, &dead_datetime), + time_delta.to_string(), + ); + } + + let reboot_datetime: Option; + let shutdown_datetime: Option; + if let Some(reboot) = &self.last_reboot_ut { + reboot_datetime = Some(reboot.login_time()); + } else { + reboot_datetime = None; + } + + if let Some(shutdown) = &self.last_shutdown_ut { + shutdown_datetime = Some(shutdown.login_time()); + } else { + shutdown_datetime = None; + } + + if shutdown_datetime.is_none() { + if ut.is_user_process() { + // If a reboot has occurred since the user logged in, but not shutdown is recorded + // then a crash must have occurred. + if reboot_datetime.is_some() && reboot_datetime.unwrap() > ut.login_time() { + ("- crash".to_string(), "".to_string()) + } else { + (" still logged in".to_string(), "".to_string()) + } + } else { + (" still running".to_string(), "".to_string()) + } + } else { + let shutdown = shutdown_datetime + .unwrap_or_else(|| time::OffsetDateTime::from_unix_timestamp(0).unwrap()); + let time_delta = duration_string(calculate_time_delta(&curr_datetime, &shutdown)); + if ut.is_user_process() { + proc_status = Some("- down"); + } + ( + self.end_time_string(proc_status, &shutdown), + time_delta.to_string(), + ) + } + } + + #[inline] + fn print_runlevel(&self, ut: &Utmpx) -> bool { + if let Some(users) = &self.users { + if !users + .iter() + .any(|val| val.as_str().trim() == ut.user().trim()) + { + return false; + } + } + if self.system { + let curr = (ut.pid() % 256) as u8 as char; + let runlvline = format!("(to lvl {curr})"); + let (end_date, delta) = self.end_state_string(ut, None); + let host = if self.dns { + find_dns_name(ut) + } else { + ut.host() + }; + self.print_line( + RUN_LEVEL_STR, + &runlvline, + &self.time_string(ut), + &host, + &end_date, + &delta, + ); + true + } else { + false + } + } + + #[inline] + fn print_shutdown(&self, ut: &Utmpx) -> bool { + if let Some(users) = &self.users { + if !users.iter().any(|val| { + val.as_str().trim() == "system down" || val.as_str().trim() == ut.user().trim() + }) { + return false; + } + } + let host = if self.dns { + find_dns_name(ut) + } else { + ut.host() + }; + if self.system { + let (end_date, delta) = self.end_state_string(ut, None); + self.print_line( + SHUTDOWN_STR, + "system down", + &self.time_string(ut), + &host, + &end_date, + &delta, + ); + true + } else { + false + } + } + + #[inline] + fn print_reboot(&self, ut: &Utmpx) -> bool { + if let Some(users) = &self.users { + if !users.iter().any(|val| { + val.as_str().trim() == ut.user().trim() || val.as_str().trim() == "system boot" + }) { + return false; + } + } + let (end_date, delta) = self.end_state_string(ut, None); + let host = if self.dns { + find_dns_name(ut) + } else { + ut.host() + }; + self.print_line( + REBOOT_STR, + "system boot", + &self.time_string(ut), + &host, + &end_date, + &delta, + ); + + true + } + + #[inline] + fn print_user(&self, ut: &Utmpx, dead_ut: Option<&Utmpx>) -> bool { + if let Some(users) = &self.users { + if !users.iter().any(|val| { + val.as_str().trim() == ut.tty_device().as_str().trim() + || val.as_str().trim() == ut.user().trim() + }) { + return false; + } + } + let mut p = PathBuf::from("/dev"); + p.push(ut.tty_device().as_str()); + let host = if self.dns { + find_dns_name(ut) + } else { + ut.host() + }; + + let (end_date, delta) = self.end_state_string(ut, dead_ut); + + self.print_line( + ut.user().as_ref(), + ut.tty_device().as_ref(), + self.time_string(ut).as_str(), + &host, + &end_date, + &delta, + ); + + true + } + + #[inline] + #[allow(clippy::too_many_arguments)] + fn print_line( + &self, + user: &str, + line: &str, + time: &str, + host: &str, + end_time: &str, + delta: &str, + ) { + let mut buf = String::with_capacity(64); + let host_to_print = host.get(0..16).unwrap_or(host); + + write!(buf, "{user:<8}").unwrap_or_default(); + write!(buf, " {line:<12}").unwrap_or_default(); + if !self.host_last && !self.no_host { + write!(buf, " {host_to_print:<16}").unwrap_or_default(); + } + + let time_size = 3 + 2 + 2 + 1 + 2; + if self.host_last && !self.no_host && self.time_format != "notime" { + write!(buf, " {time: UResult<()> { + let _matches = uu_app().try_get_matches_from(args)?; + + println!("unsupported command on Windows"); + Ok(()) +} diff --git a/tests/by-util/test_last.rs b/tests/by-util/test_last.rs new file mode 100644 index 0000000..b0ca6ba --- /dev/null +++ b/tests/by-util/test_last.rs @@ -0,0 +1,111 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. +// spell-checker:ignore (words) symdir somefakedir + +use crate::common::util::TestScenario; + +use regex::Regex; +use std::fs; +use std::io::Write; + +#[test] +#[cfg(unix)] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[test] +#[cfg(unix)] +fn test_last() { + let regex = Regex::new("still running|still logged in").unwrap(); + TestScenario::new(util_name!()) + .ucmd() + .succeeds() + .stdout_matches(®ex); +} + +#[test] +#[cfg(unix)] +fn test_limit_arg() { + let line_check = |input: &str| input.lines().count() == 3; + new_ucmd!() + .arg("--limit=1") + .succeeds() + .stdout_str_check(line_check); +} + +#[test] +// The -x flag generally adds two rows "shutdown" and "runlevel" +// "shutdown" cannot be checked for since not every machine will have shutdown +// "runlevel" only makes sense for Linux systems, so only Linux is included for +// this test. +#[cfg(target_os = "linux")] +#[ignore = "fails on Arch Linux"] +fn test_system_arg() { + new_ucmd!().arg("-x").succeeds().stdout_contains("runlevel"); +} + +#[test] +#[cfg(unix)] +fn test_timestamp_format_no_time() { + let regex = Regex::new(" [0-9][0-9]:[0-9][0-9] ").unwrap(); + new_ucmd!() + .arg("--time-format=notime") + .succeeds() + .stdout_does_not_match(®ex); +} + +#[test] +#[cfg(unix)] +fn test_timestamp_format_short() { + let regex = Regex::new(" [0-9][0-9]:[0-9][0-9] ").unwrap(); + new_ucmd!() + .arg("--time-format=short") + .succeeds() + .stdout_matches(®ex); +} + +#[test] +#[cfg(unix)] +fn test_timestamp_format_full() { + let regex = Regex::new(" [0-9][0-9]:[0-9][0-9]:[0-9][0-9] ").unwrap(); + new_ucmd!() + .arg("--time-format=full") + .succeeds() + .stdout_matches(®ex); +} + +// 2024-07-11T19:30:44+08:00 +#[test] +#[cfg(unix)] +fn test_timestamp_format_iso() { + let regex = + Regex::new(" [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]") + .unwrap(); + new_ucmd!() + .arg("--time-format=iso") + .succeeds() + .stdout_matches(®ex); +} + +#[test] +#[cfg(unix)] +fn test_short_invalid_utmp_file() { + let filepath = "/tmp/testfile"; + let testfile = fs::File::create(filepath); + // Random bytes + let data: Vec = vec![ + 4, 5, 6, 16, 8, 13, 2, 12, 5, 3, 11, 5, 1, 13, 1, 1, 0, 9, 5, 5, 2, 8, 4, + ]; + let _ = testfile.unwrap().write_all(&data); + + let regex = Regex::new(r"\n\S*\sbegins\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s*[0-9][0-9]?\s*[0-9][0-9]:[0-9][0-9]:[0-9][0-9]\s*[0-9]*") + .unwrap(); + + new_ucmd!() + .arg(format!("--file={filepath}")) + .succeeds() + .stdout_matches(®ex); +} diff --git a/tests/tests.rs b/tests/tests.rs index 435dc14..57e9513 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -24,3 +24,7 @@ mod test_ctrlaltdel; #[cfg(feature = "rev")] #[path = "by-util/test_rev.rs"] mod test_rev; + +#[cfg(feature = "last")] +#[path = "by-util/test_last.rs"] +mod test_last;