From 8ef22a0811a9f4c0f7797ee915674d083913ef98 Mon Sep 17 00:00:00 2001 From: jbosslady Date: Tue, 23 Jun 2026 02:16:55 +0200 Subject: [PATCH 1/7] wall: initial implementation --- Cargo.lock | 31 ++-- Cargo.toml | 1 + src/uu/wall/Cargo.toml | 19 +++ src/uu/wall/locales/en-US.ftl | 18 ++ src/uu/wall/locales/fr-FR.ftl | 18 ++ src/uu/wall/src/main.rs | 6 + src/uu/wall/src/wall.rs | 303 ++++++++++++++++++++++++++++++++++ 7 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 src/uu/wall/Cargo.toml create mode 100644 src/uu/wall/locales/en-US.ftl create mode 100644 src/uu/wall/locales/fr-FR.ftl create mode 100644 src/uu/wall/src/main.rs create mode 100644 src/uu/wall/src/wall.rs diff --git a/Cargo.lock b/Cargo.lock index 0f364dfe..dd728783 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -380,9 +380,6 @@ name = "deranged" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", -] [[package]] name = "diff" @@ -898,9 +895,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" @@ -1400,12 +1397,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", - "itoa", "libc", "num-conv", "num_threads", @@ -1417,15 +1413,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -1555,6 +1551,7 @@ dependencies = [ "uu_setpgid", "uu_setsid", "uu_uuidgen", + "uu_wall", "uucore 0.2.2", "uuhelp_parser", "uuid", @@ -1782,6 +1779,16 @@ dependencies = [ "windows", ] +[[package]] +name = "uu_wall" +version = "0.0.1" +dependencies = [ + "clap", + "nix 0.31.3", + "thiserror", + "uucore 0.2.2", +] + [[package]] name = "uucore" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index a35f33c5..5fd2c114 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -121,6 +121,7 @@ rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/r setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" } setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path ="src/uu/uuidgen" } +wall = {option = true, version = "0.0.1", package = "uu_wall", path = "src/uu/wall" } [dev-dependencies] ctor = "1.0.0" diff --git a/src/uu/wall/Cargo.toml b/src/uu/wall/Cargo.toml new file mode 100644 index 00000000..00dc2f17 --- /dev/null +++ b/src/uu/wall/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "uu_wall" +version = "0.0.1" +description = "wall ~ write a message to all users" + +edition = "2021" + +[lib] +path = "src/wall.rs" + +[[bin]] +name = "wall" +path = "src/main.rs" + +[dependencies] +clap.workspace = true +nix = { workspace = true, features = ["feature"] } +thiserror.workspace = true +uucore = { workspace = true, features = ["utmpx"] } diff --git a/src/uu/wall/locales/en-US.ftl b/src/uu/wall/locales/en-US.ftl new file mode 100644 index 00000000..218d0e54 --- /dev/null +++ b/src/uu/wall/locales/en-US.ftl @@ -0,0 +1,18 @@ +wall-about = Write a message to all users +wall-usage = wall [message | file] + +# Help messages +wall-help-group = Limit printing messages to members of group defined as group argument. The argument can be a group name or GID +wall-help-nobanner = Suppress the banner +wall-help-timeout = Abandon the write attempt to the terminal after timeout seconds. + +# Error messages +wall-error-no-such-file = { $program }: can'et read { $file }: No such file or directory +wall-encoding-error = { $program }: data could not be decoded as UTF-8 +wall-error-stdin = { $program }: can't read standard input +wall-error-osstring = { $program }: can't convert OS string to String +wall-error-mac-os-too-many-args = { $program }: too many arguments, only one argument is expected +wall-error-open-terminal = { $program }: can't open terminal for writing +wall-error-write-terminal = { $program }: can't write into terminal +wall-unknown-date = unknown date +wall-unknown-tty = unknown origin diff --git a/src/uu/wall/locales/fr-FR.ftl b/src/uu/wall/locales/fr-FR.ftl new file mode 100644 index 00000000..a651bb40 --- /dev/null +++ b/src/uu/wall/locales/fr-FR.ftl @@ -0,0 +1,18 @@ +wall-about = Écrit un message à tous les utilisateurs +wall-usage = wall [message | fichier] + +# Messages d'aide +wall-help-group = Limite l'affichqge du message aux membres du groupe défini par l'argument. L'argument peut être le nom d'un groupe ou son ID. +wall-help-nobanner = Supprime la bannière +wall-help-timeout = Abandonne la tentative d'écriture dans le terminal apres le temps écoulé + +# Messages d'erreur +wall-error-no-such-file = { $program }: ne peut pas lire { $file }: Aucun fichier ou répertoire de ce type +wall-encoding-error = { $program }: échec de la conversion vers UTF-8 +wall-error-stdin = { $program }: échec de la lecture dans le terminal +wall-error-osstring = { $program }: échec de la conversion de la chaîne OS en chaîne +wall-error-mac-os-too-many-args = { $program }: trop d'arguments, un seul argument est attendu +wall-error-open-terminal = { $program }: échec de l'ouverture du terminal pour l'écriture +wall-error-write-terminal = { $program }: échec de l'écrite dans le terminal +wall-unknown-date = date inconnue +wall-unknown-tty = origine inconnue diff --git a/src/uu/wall/src/main.rs b/src/uu/wall/src/main.rs new file mode 100644 index 00000000..c57b1120 --- /dev/null +++ b/src/uu/wall/src/main.rs @@ -0,0 +1,6 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +uucore::bin!(uu_wall); diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs new file mode 100644 index 00000000..4c6a8869 --- /dev/null +++ b/src/uu/wall/src/wall.rs @@ -0,0 +1,303 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::builder::ValueParser; +use clap::parser::ValuesRef; +use clap::{Arg, ArgAction, Command}; +use nix::sys::utsname; +use std::env; +use std::ffi::OsString; +use std::io; +use std::io::prelude::*; +use std::string::FromUtf8Error; +use thiserror::Error; + +use uucore::error::{UError, UResult}; +use uucore::format_usage; +use uucore::utmpx::Utmpx; + +use uucore::translate; +const STRING: &str = "string"; +const OPT_GROUP: &str = "group"; +const OPT_NOBANNER: &str = "nobanner"; +const OPT_TIMEOUT: &str = "timeout"; + +#[derive(Error, Debug)] +enum WallError { + #[error("{}", translate!("wall-error-stdin"))] + Stdin(#[from] io::Error), + #[error("{}", translate!("wall-encoding-error"))] + VecToString(#[from] FromUtf8Error), + #[error("{}", translate!("wall-error-osstring"))] + ToStringError, + #[error("{}", translate!("wall-error-mac-os-too-many-args"))] + MacOsTooManyArgs, +} + +impl UError for WallError { + fn code(&self) -> i32 { + 1 + } +} + +#[uucore::main(no_signals)] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = args.skip(1).peekable(); + let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + let message = get_message(matches.get_many(STRING).unwrap_or_default())?; + let users = find_logged_users(); + write_to_terminals(message, users)?; + Ok(()) +} + +pub fn uu_app() -> Command { + Command::new("wall") + .version(uucore::crate_version!()) + .about(translate!("wall-about")) + .override_usage(format_usage(&translate!("pwd-usage"))) + .arg( + Arg::new(OPT_GROUP) // TODO(FEAT): Implement -g/--groups to target specific + // users inside a group + .short('g') + .long(OPT_GROUP) + .value_name("GROUP") + .help(translate!("wall-help-group")) + .num_args(1) + .action(ArgAction::Append) // User can target more than one group + .value_parser(clap::value_parser!(String)), + ) + .arg( + Arg::new(OPT_NOBANNER) // TODO(FEAT): Implement -n/--nobanner to remove broadcasting + // intro message + .short('n') + .long(OPT_NOBANNER) + .action(ArgAction::SetTrue) + .help(translate!("wall-help-nobanner")), + ) + .arg( + Arg::new(OPT_TIMEOUT) // TODO(FEAT): Implement -t --timeout to stop trying to print + // after passed a delay + .short('t') + .long(OPT_TIMEOUT) + .value_name("SECONDS") + .help(translate!("wall-help-timeout")) + .num_args(1), + ) + .arg( + Arg::new(STRING) + .action(ArgAction::Append) + .value_parser(ValueParser::os_string()), + ) +} + +fn get_message(args: ValuesRef) -> Result { + if args.len() == 0 { + read_from_stdin() + } else if args.len() == 1 { + read_from_file(args.into_iter().next().unwrap()) + } else if cfg!(target_os = "macos") { + Err(WallError::MacOsTooManyArgs) + } else { + concatenate_message(args) + } +} + +fn read_from_stdin() -> Result { + let mut buffer = Vec::new(); + io::stdin().read_to_end(&mut buffer)?; + let res = String::from_utf8(buffer)?; + Ok(res) +} + +fn read_from_file(file: &OsString) -> Result { + let mut buffer = Vec::new(); + let mut file = std::fs::File::open(file)?; + file.read_to_end(&mut buffer)?; + let res = String::from_utf8(buffer)?; + Ok(res) +} + +fn concatenate_message(args: ValuesRef) -> Result { + let mut res = String::new(); + for arg in args { + res.push_str(arg.to_str().ok_or(WallError::ToStringError)?); + res.push(' '); + } + res.pop(); + Ok(res) +} + +fn find_logged_users() -> Vec { + let mut res = Vec::::new(); + for ut in Utmpx::iter_all_records() { + if ut.is_user_process() { + let mut tty_path = OsString::from("/dev/"); + tty_path.push(OsString::from(&ut.tty_device().clone())); + res.push(tty_path); + } + } + res +} + +fn wall_intro_message() -> String { + let user = "USER"; + let biding = match nix::sys::utsname::uname() { + Ok(uts) => match uts.nodename() { + Ok(hostname) => hostname, + Err(_) => String::from(""), + }, + Err(_) => String::from(""), + }; + + let user = env::var_os(user).unwrap_or_default(); + // Fetch the TTY of the process calling wall (requires OS-specific calls or a wrapper function) + let tty = "/dev/".to_owned() + &get_sender(); + + let datetime = get_hour_and_date(); + #[cfg(target_os = "linux")] + return format!( + "\r\nBroadcast message from {}@{hostname} ({tty}) at {datetime} \r\n\r\n", + user.to_string_lossy() + ); + #[cfg(target_os = "macos")] + return format!( + "\r\nBroadcast message from {}@{hostname}\r\n\t({tty}) at {datetime}\r\n\r\n", + user.to_string_lossy() + ); +} + +fn write_to_terminals(message: String, users: Vec) -> UResult<()> { + let transmission = wall_intro_message() + &message + "\r\n\r\n"; + for user in users { + let mut file = match std::fs::OpenOptions::new().write(true).open(user) { + Ok(f) => f, + Err(e) => { + eprintln!("{}: {e}", translate!("wall-error-open-terminal")); + continue; + } + }; + write!(file, "{transmission}").map_err(|e| { + eprintln!("{}:, {e}", translate!("wall-error-write-terminal")); + WallError::Stdin(e) + })?; + } + Ok(()) +} + +fn get_hour_and_date() -> String { + #[cfg(target_os = "linux")] + return Zoned::now().strftime("(%a %b %d %H:%M:%S %Y):").to_string(); + #[cfg(target_os = "macos")] + return Zoned::now().strftime("%H:%M %Z...").to_string(); +} + +fn get_sender() -> String { + rustix::termios::ttyname(io::stdin(), Vec::with_capacity(16)) + .map(|s| s.to_string_lossy().trim_start_matches("/dev/").to_owned()) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + + use crate::{OPT_GROUP, STRING}; + use crate::{find_logged_users, get_message, uu_app, write_to_terminals}; + use std::ffi::OsString; + use std::process::{Command, Output}; + + #[test] + fn test_basic_clap_implementation() { + let group = String::from("staff"); + let file = String::from("LICENSE"); + let command = vec!["wall", "-g", &group, &file]; + let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) + .expect("Error outside of test perimeter"); + assert!(matches.get_one::(OPT_GROUP).unwrap() == &group); + assert!( + matches + .get_one::(STRING) + .unwrap() + .clone() + .into_string() + .unwrap() + == file + ); + } + + #[test] + fn test_get_message_on_file() { + let file = String::from("LICENSE"); + + // wall does not print the content of the file in the stdout, it sends it to the tty(s) + // Hence the use of cat to check if the get_message function can extract correctly the + // file + let mut command = Command::new("cat"); + command.arg(&file); + let output: Output = command.output().expect("Failed to start 'cat' command"); + assert!( + output.status.success(), + "'cat' command exit with failure status" + ); + let command_output = + String::from_utf8(output.stdout).expect("Failed to convert 'cat'output"); + + let command = vec!["wall", &file]; + let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) + .expect("External error"); + let pos_arg = matches.get_many(STRING).unwrap_or_default(); + let function_output = get_message(pos_arg).unwrap(); + assert_eq!(function_output, command_output); + } + + #[test] + fn test_get_message_on_stdin() { + // for the moment test against cat is not implemented + let command = vec!["wall"]; + let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) + .expect("External error"); + let pos_arg = matches.get_many(STRING).unwrap_or_default(); + let function_output = get_message(pos_arg).unwrap(); + assert_eq!(function_output, "Hello !\n"); + } + + #[test] + fn test_arguments_as_message() { + let command = vec!["wall", "Hello", "World", "!"]; + let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) + .expect("External error"); + let pos_arg = matches.get_many(STRING).unwrap_or_default(); + let function_output = get_message(pos_arg).unwrap(); + assert_eq!(function_output, "Hello World !"); + } + + #[test] + fn test_found_connected_users() { + let users = find_logged_users(); + assert_eq!( + users, + vec!( + OsString::from("tty1"), + OsString::from("tty2"), + OsString::from("tty3") + ) + ); + } + + #[test] + fn test_print_to_terminals() { + let users = find_logged_users(); + let _ = write_to_terminals(String::from("hello world!"), users); + let _ = write_to_terminals( + String::from("hello world!"), + vec![OsString::from("/dev/tty1")], + ); + } + + #[test] + fn test_get_sender() { + let sender = crate::get_sender(); + assert_eq!(sender, "pts/0"); + } +} From cf69236b7256950b95b359e12908454242c7e151 Mon Sep 17 00:00:00 2001 From: jlepany Date: Tue, 23 Jun 2026 13:36:03 +0200 Subject: [PATCH 2/7] wall: update used crates, delete locales --- Cargo.lock | 1 + src/uu/wall/Cargo.toml | 3 ++- src/uu/wall/locales/en-US.ftl | 18 -------------- src/uu/wall/locales/fr-FR.ftl | 18 -------------- src/uu/wall/src/wall.rs | 46 ++++++++++++----------------------- 5 files changed, 18 insertions(+), 68 deletions(-) delete mode 100644 src/uu/wall/locales/en-US.ftl delete mode 100644 src/uu/wall/locales/fr-FR.ftl diff --git a/Cargo.lock b/Cargo.lock index dd728783..d5ee3833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1783,6 +1783,7 @@ dependencies = [ name = "uu_wall" version = "0.0.1" dependencies = [ + "chrono", "clap", "nix 0.31.3", "thiserror", diff --git a/src/uu/wall/Cargo.toml b/src/uu/wall/Cargo.toml index 00dc2f17..242135f2 100644 --- a/src/uu/wall/Cargo.toml +++ b/src/uu/wall/Cargo.toml @@ -13,7 +13,8 @@ name = "wall" path = "src/main.rs" [dependencies] +chrono = { workspace = true } clap.workspace = true -nix = { workspace = true, features = ["feature"] } +nix = { workspace = true, features = ["feature", "fs", "hostname", "term"] } thiserror.workspace = true uucore = { workspace = true, features = ["utmpx"] } diff --git a/src/uu/wall/locales/en-US.ftl b/src/uu/wall/locales/en-US.ftl deleted file mode 100644 index 218d0e54..00000000 --- a/src/uu/wall/locales/en-US.ftl +++ /dev/null @@ -1,18 +0,0 @@ -wall-about = Write a message to all users -wall-usage = wall [message | file] - -# Help messages -wall-help-group = Limit printing messages to members of group defined as group argument. The argument can be a group name or GID -wall-help-nobanner = Suppress the banner -wall-help-timeout = Abandon the write attempt to the terminal after timeout seconds. - -# Error messages -wall-error-no-such-file = { $program }: can'et read { $file }: No such file or directory -wall-encoding-error = { $program }: data could not be decoded as UTF-8 -wall-error-stdin = { $program }: can't read standard input -wall-error-osstring = { $program }: can't convert OS string to String -wall-error-mac-os-too-many-args = { $program }: too many arguments, only one argument is expected -wall-error-open-terminal = { $program }: can't open terminal for writing -wall-error-write-terminal = { $program }: can't write into terminal -wall-unknown-date = unknown date -wall-unknown-tty = unknown origin diff --git a/src/uu/wall/locales/fr-FR.ftl b/src/uu/wall/locales/fr-FR.ftl deleted file mode 100644 index a651bb40..00000000 --- a/src/uu/wall/locales/fr-FR.ftl +++ /dev/null @@ -1,18 +0,0 @@ -wall-about = Écrit un message à tous les utilisateurs -wall-usage = wall [message | fichier] - -# Messages d'aide -wall-help-group = Limite l'affichqge du message aux membres du groupe défini par l'argument. L'argument peut être le nom d'un groupe ou son ID. -wall-help-nobanner = Supprime la bannière -wall-help-timeout = Abandonne la tentative d'écriture dans le terminal apres le temps écoulé - -# Messages d'erreur -wall-error-no-such-file = { $program }: ne peut pas lire { $file }: Aucun fichier ou répertoire de ce type -wall-encoding-error = { $program }: échec de la conversion vers UTF-8 -wall-error-stdin = { $program }: échec de la lecture dans le terminal -wall-error-osstring = { $program }: échec de la conversion de la chaîne OS en chaîne -wall-error-mac-os-too-many-args = { $program }: trop d'arguments, un seul argument est attendu -wall-error-open-terminal = { $program }: échec de l'ouverture du terminal pour l'écriture -wall-error-write-terminal = { $program }: échec de l'écrite dans le terminal -wall-unknown-date = date inconnue -wall-unknown-tty = origine inconnue diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index 4c6a8869..4586a50f 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -3,14 +3,16 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use chrono; use clap::builder::ValueParser; use clap::parser::ValuesRef; use clap::{Arg, ArgAction, Command}; -use nix::sys::utsname; +use nix::unistd; use std::env; use std::ffi::OsString; use std::io; use std::io::prelude::*; +use std::os::fd::AsFd; use std::string::FromUtf8Error; use thiserror::Error; @@ -32,8 +34,6 @@ enum WallError { VecToString(#[from] FromUtf8Error), #[error("{}", translate!("wall-error-osstring"))] ToStringError, - #[error("{}", translate!("wall-error-mac-os-too-many-args"))] - MacOsTooManyArgs, } impl UError for WallError { @@ -97,8 +97,6 @@ fn get_message(args: ValuesRef) -> Result { read_from_stdin() } else if args.len() == 1 { read_from_file(args.into_iter().next().unwrap()) - } else if cfg!(target_os = "macos") { - Err(WallError::MacOsTooManyArgs) } else { concatenate_message(args) } @@ -143,28 +141,19 @@ fn find_logged_users() -> Vec { fn wall_intro_message() -> String { let user = "USER"; - let biding = match nix::sys::utsname::uname() { - Ok(uts) => match uts.nodename() { - Ok(hostname) => hostname, - Err(_) => String::from(""), - }, - Err(_) => String::from(""), - }; + let biding = unistd::gethostname().unwrap_or_else(|_| "".into()); + let hostname = biding.to_string_lossy(); let user = env::var_os(user).unwrap_or_default(); // Fetch the TTY of the process calling wall (requires OS-specific calls or a wrapper function) - let tty = "/dev/".to_owned() + &get_sender(); + let tty = &get_sender(); let datetime = get_hour_and_date(); #[cfg(target_os = "linux")] return format!( - "\r\nBroadcast message from {}@{hostname} ({tty}) at {datetime} \r\n\r\n", - user.to_string_lossy() - ); - #[cfg(target_os = "macos")] - return format!( - "\r\nBroadcast message from {}@{hostname}\r\n\t({tty}) at {datetime}\r\n\r\n", - user.to_string_lossy() + "\r\nBroadcast message from {}@{} ({tty}) at ({datetime}) \r\n\r\n", + user.to_string_lossy(), + hostname ); } @@ -187,16 +176,11 @@ fn write_to_terminals(message: String, users: Vec) -> UResult<()> { } fn get_hour_and_date() -> String { - #[cfg(target_os = "linux")] - return Zoned::now().strftime("(%a %b %d %H:%M:%S %Y):").to_string(); - #[cfg(target_os = "macos")] - return Zoned::now().strftime("%H:%M %Z...").to_string(); + chrono::Local::now().format("%H:%M %Z %a %b %e").to_string() } fn get_sender() -> String { - rustix::termios::ttyname(io::stdin(), Vec::with_capacity(16)) - .map(|s| s.to_string_lossy().trim_start_matches("/dev/").to_owned()) - .unwrap_or_default() + unistd::ttyname(std::io::stdin().as_fd()).unwrap_or_else(|_| "".into()).to_string_lossy().to_string() } #[cfg(test)] @@ -228,7 +212,7 @@ mod tests { #[test] fn test_get_message_on_file() { - let file = String::from("LICENSE"); + let file = String::from("Cargo.toml"); // wall does not print the content of the file in the stdout, it sends it to the tty(s) // Hence the use of cat to check if the get_message function can extract correctly the @@ -278,9 +262,9 @@ mod tests { assert_eq!( users, vec!( - OsString::from("tty1"), - OsString::from("tty2"), - OsString::from("tty3") + OsString::from("/dev/tty2"), + OsString::from("/dev/pts/1"), + OsString::from("/dev/pts/2") ) ); } From 526f7af98760b84bc8f1f58410b0c85fdb7d1db9 Mon Sep 17 00:00:00 2001 From: jlepany Date: Tue, 23 Jun 2026 14:19:53 +0200 Subject: [PATCH 3/7] wall: change date format and add new check to avoid writing into wrong terminal --- src/uu/wall/src/wall.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index 4586a50f..e7b4a785 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -141,7 +141,7 @@ fn find_logged_users() -> Vec { fn wall_intro_message() -> String { let user = "USER"; - let biding = unistd::gethostname().unwrap_or_else(|_| "".into()); + let biding = unistd::gethostname().unwrap_or_else(|_| "".into()); let hostname = biding.to_string_lossy(); let user = env::var_os(user).unwrap_or_default(); @@ -162,11 +162,11 @@ fn write_to_terminals(message: String, users: Vec) -> UResult<()> { for user in users { let mut file = match std::fs::OpenOptions::new().write(true).open(user) { Ok(f) => f, - Err(e) => { - eprintln!("{}: {e}", translate!("wall-error-open-terminal")); - continue; - } + Err(_) => continue, }; + if !unistd::isatty(&file).unwrap_or(false) { + continue; + } write!(file, "{transmission}").map_err(|e| { eprintln!("{}:, {e}", translate!("wall-error-write-terminal")); WallError::Stdin(e) @@ -176,18 +176,21 @@ fn write_to_terminals(message: String, users: Vec) -> UResult<()> { } fn get_hour_and_date() -> String { - chrono::Local::now().format("%H:%M %Z %a %b %e").to_string() + chrono::Local::now().format("%a %b %e %H:%M %Z").to_string() } fn get_sender() -> String { - unistd::ttyname(std::io::stdin().as_fd()).unwrap_or_else(|_| "".into()).to_string_lossy().to_string() + unistd::ttyname(std::io::stdin().as_fd()) + .unwrap_or_else(|_| "".into()) + .to_string_lossy() + .to_string() } #[cfg(test)] mod tests { - use crate::{OPT_GROUP, STRING}; use crate::{find_logged_users, get_message, uu_app, write_to_terminals}; + use crate::{OPT_GROUP, STRING}; use std::ffi::OsString; use std::process::{Command, Output}; From d3b29532e08d360ae5168b9feb0f12334271d25f Mon Sep 17 00:00:00 2001 From: jbosslady Date: Wed, 24 Jun 2026 14:56:45 +0200 Subject: [PATCH 4/7] wall: modify broadcast message for visibility inside terminal and add wall to global Cargo.toml --- Cargo.toml | 3 ++- src/uu/wall/src/wall.rs | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5fd2c114..ba1f4757 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ feat_common_core = [ "setpgid", "setsid", "uuidgen", + "wall", ] [workspace.dependencies] @@ -121,7 +122,7 @@ rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/r setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" } setsid = { optional = true, version = "0.0.1", package = "uu_setsid", path ="src/uu/setsid" } uuidgen = { optional = true, version = "0.0.1", package = "uu_uuidgen", path ="src/uu/uuidgen" } -wall = {option = true, version = "0.0.1", package = "uu_wall", path = "src/uu/wall" } +wall = { optional = true, version = "0.0.1", package = "uu_wall", path = "src/uu/wall" } [dev-dependencies] ctor = "1.0.0" diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index e7b4a785..0bfab36a 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -28,11 +28,11 @@ const OPT_TIMEOUT: &str = "timeout"; #[derive(Error, Debug)] enum WallError { - #[error("{}", translate!("wall-error-stdin"))] + #[error("wall: cannot read stdin")] Stdin(#[from] io::Error), - #[error("{}", translate!("wall-encoding-error"))] + #[error("wall: encoding error")] VecToString(#[from] FromUtf8Error), - #[error("{}", translate!("wall-error-osstring"))] + #[error("wall: osstring conversion failed")] ToStringError, } @@ -63,7 +63,7 @@ pub fn uu_app() -> Command { .short('g') .long(OPT_GROUP) .value_name("GROUP") - .help(translate!("wall-help-group")) + .help("Send restrict to only users in the group(s)") .num_args(1) .action(ArgAction::Append) // User can target more than one group .value_parser(clap::value_parser!(String)), @@ -74,7 +74,7 @@ pub fn uu_app() -> Command { .short('n') .long(OPT_NOBANNER) .action(ArgAction::SetTrue) - .help(translate!("wall-help-nobanner")), + .help("Suppress the intro branner of the broadcast"), ) .arg( Arg::new(OPT_TIMEOUT) // TODO(FEAT): Implement -t --timeout to stop trying to print @@ -82,7 +82,7 @@ pub fn uu_app() -> Command { .short('t') .long(OPT_TIMEOUT) .value_name("SECONDS") - .help(translate!("wall-help-timeout")) + .help("Abandon after t seconds the write attempt to the terminals") .num_args(1), ) .arg( @@ -149,16 +149,16 @@ fn wall_intro_message() -> String { let tty = &get_sender(); let datetime = get_hour_and_date(); - #[cfg(target_os = "linux")] - return format!( + format!( "\r\nBroadcast message from {}@{} ({tty}) at ({datetime}) \r\n\r\n", user.to_string_lossy(), hostname - ); + ) } fn write_to_terminals(message: String, users: Vec) -> UResult<()> { - let transmission = wall_intro_message() + &message + "\r\n\r\n"; + let format_message = message.replace("\n", "\r\n\n"); + let transmission = wall_intro_message() + &format_message; for user in users { let mut file = match std::fs::OpenOptions::new().write(true).open(user) { Ok(f) => f, @@ -168,7 +168,7 @@ fn write_to_terminals(message: String, users: Vec) -> UResult<()> { continue; } write!(file, "{transmission}").map_err(|e| { - eprintln!("{}:, {e}", translate!("wall-error-write-terminal")); + eprintln!("wall-error: terminal write:, {e}",); WallError::Stdin(e) })?; } @@ -189,8 +189,8 @@ fn get_sender() -> String { #[cfg(test)] mod tests { - use crate::{find_logged_users, get_message, uu_app, write_to_terminals}; use crate::{OPT_GROUP, STRING}; + use crate::{find_logged_users, get_message, uu_app, write_to_terminals}; use std::ffi::OsString; use std::process::{Command, Output}; From 7315ad5cdc810a0ba24f474ec1d86ea366e3fc76 Mon Sep 17 00:00:00 2001 From: jbosslady Date: Mon, 27 Jul 2026 14:27:03 +0200 Subject: [PATCH 5/7] wall: spliting macos and linux usage version. introduction of global test into the library --- src/uu/wall/src/wall.rs | 208 +++++++++++++++++-------------------- src/uu/wall/wall.md | 1 + tests/by-util/test_wall.rs | 92 ++++++++++++++++ tests/tests.rs | 4 + 4 files changed, 195 insertions(+), 110 deletions(-) create mode 100644 src/uu/wall/wall.md create mode 100644 tests/by-util/test_wall.rs diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index 0bfab36a..30c20f42 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -3,37 +3,62 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use chrono; use clap::builder::ValueParser; use clap::parser::ValuesRef; use clap::{Arg, ArgAction, Command}; +#[cfg(unix)] use nix::unistd; use std::env; use std::ffi::OsString; use std::io; use std::io::prelude::*; +#[cfg(unix)] use std::os::fd::AsFd; use std::string::FromUtf8Error; use thiserror::Error; -use uucore::error::{UError, UResult}; -use uucore::format_usage; -use uucore::utmpx::Utmpx; +#[cfg(unix)] +use uucore::{ + error::{UError, UResult}, + format_usage, + utmpx::Utmpx, + translate // unused at the moment... +}; -use uucore::translate; const STRING: &str = "string"; const OPT_GROUP: &str = "group"; const OPT_NOBANNER: &str = "nobanner"; const OPT_TIMEOUT: &str = "timeout"; +#[cfg(target_os = "macos")] +mod options { + use super::OPT_GROUP; // module don't automatically has access to const of parent + + pub const VALID_SHORT: &[char] = &['g']; + pub const VALID_LONG: &[&str] = &[OPT_GROUP]; +} + + +#[cfg(target_os = "linux")] +mod options { + use super::{OPT_GROUP, OPT_NOBANNER, OPT_TIMEOUT}; + + pub const VALID_SHORT: &[char] = &['g', 'n', 't']; + pub const VALID_LONG: &[&str] = &[OPT_GROUP, OPT_NOBANNER, OPT_TIMEOUT]; +} + #[derive(Error, Debug)] enum WallError { + #[error("wall: invalid argument")] + ArgError, #[error("wall: cannot read stdin")] Stdin(#[from] io::Error), #[error("wall: encoding error")] VecToString(#[from] FromUtf8Error), #[error("wall: osstring conversion failed")] ToStringError, + #[error("wall is not supported on windows")] + WindowsError, } impl UError for WallError { @@ -42,9 +67,17 @@ impl UError for WallError { } } +#[cfg(target_family = "unix")] #[uucore::main(no_signals)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { + #[cfg(windows)] + return Err(io::Error::new( + WallError::WindowsError)); let args = args.skip(1).peekable(); + match args_pre_scan(&args) { + Ok(_) => {} + Err(e) => { return Err(WallError::ArgError); } + } let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let message = get_message(matches.get_many(STRING).unwrap_or_default())?; let users = find_logged_users(); @@ -52,11 +85,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(()) } +#[cfg(not(target_family = "unix"))] +#[uucore::main(no_signals)] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let _matches: ArgMatches = uu_app().try_get_matches_from(args)?; + Err(uucore::error::USimpleError::new( + 1, + "`wall` is available only on Unix platforms." + )) +} + +#[cfg(target_os = "linux")] pub fn uu_app() -> Command { Command::new("wall") .version(uucore::crate_version!()) - .about(translate!("wall-about")) - .override_usage(format_usage(&translate!("pwd-usage"))) + .about(translate!("wall.md")) + .infer_long_args(true) + .override_usage(format_usage(&translate!("wall.md"))) .arg( Arg::new(OPT_GROUP) // TODO(FEAT): Implement -g/--groups to target specific // users inside a group @@ -65,6 +110,7 @@ pub fn uu_app() -> Command { .value_name("GROUP") .help("Send restrict to only users in the group(s)") .num_args(1) + .required(false) .action(ArgAction::Append) // User can target more than one group .value_parser(clap::value_parser!(String)), ) @@ -73,6 +119,7 @@ pub fn uu_app() -> Command { // intro message .short('n') .long(OPT_NOBANNER) + .required(false) .action(ArgAction::SetTrue) .help("Suppress the intro branner of the broadcast"), ) @@ -81,6 +128,7 @@ pub fn uu_app() -> Command { // after passed a delay .short('t') .long(OPT_TIMEOUT) + .required(false) .value_name("SECONDS") .help("Abandon after t seconds the write attempt to the terminals") .num_args(1), @@ -92,6 +140,43 @@ pub fn uu_app() -> Command { ) } +#[cfg(target_os = "macos")] +pub fn uu_app() -> Command { + Command::new("wall") + .version(uucore::crate_version!()) + .about(translate!("wall.md")) + .infer_long_args(true) + .override_usage(format_usage(&translate!("wall.md"))) + .arg( + Arg::new(OPT_GROUP) // TODO(FEAT): Implement -g/--groups to target specific + // users inside a group + .short('g') + .long(OPT_GROUP) + .value_name("GROUP") + .help("Send restrict to only users in the group(s)") + .num_args(1) + .required(false) + .action(ArgAction::Append) // User can target more than one group + .value_parser(clap::value_parser!(String)), + ) + .arg( + Arg::new(STRING) + .action(ArgAction::Append) + .value_parser(ValueParser::os_string()), + ) +} + +fn args_pre_scan(args: &ValuesRef) -> Result<(), String> { + for arg in args { + let arg = arg.to_string_lossy(); + if arg == "--" { + break; + } + + } + Ok(()) +} + fn get_message(args: ValuesRef) -> Result { if args.len() == 0 { read_from_stdin() @@ -175,6 +260,12 @@ fn write_to_terminals(message: String, users: Vec) -> UResult<()> { Ok(()) } +#[cfg(target_os = "linux")] +fn get_hour_and_date() -> String { + chrono::Local::now().format("%a %b %e %H:%M %Z").to_string() +} + +#[cfg(target_os = "macos")] fn get_hour_and_date() -> String { chrono::Local::now().format("%a %b %e %H:%M %Z").to_string() } @@ -185,106 +276,3 @@ fn get_sender() -> String { .to_string_lossy() .to_string() } - -#[cfg(test)] -mod tests { - - use crate::{OPT_GROUP, STRING}; - use crate::{find_logged_users, get_message, uu_app, write_to_terminals}; - use std::ffi::OsString; - use std::process::{Command, Output}; - - #[test] - fn test_basic_clap_implementation() { - let group = String::from("staff"); - let file = String::from("LICENSE"); - let command = vec!["wall", "-g", &group, &file]; - let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) - .expect("Error outside of test perimeter"); - assert!(matches.get_one::(OPT_GROUP).unwrap() == &group); - assert!( - matches - .get_one::(STRING) - .unwrap() - .clone() - .into_string() - .unwrap() - == file - ); - } - - #[test] - fn test_get_message_on_file() { - let file = String::from("Cargo.toml"); - - // wall does not print the content of the file in the stdout, it sends it to the tty(s) - // Hence the use of cat to check if the get_message function can extract correctly the - // file - let mut command = Command::new("cat"); - command.arg(&file); - let output: Output = command.output().expect("Failed to start 'cat' command"); - assert!( - output.status.success(), - "'cat' command exit with failure status" - ); - let command_output = - String::from_utf8(output.stdout).expect("Failed to convert 'cat'output"); - - let command = vec!["wall", &file]; - let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) - .expect("External error"); - let pos_arg = matches.get_many(STRING).unwrap_or_default(); - let function_output = get_message(pos_arg).unwrap(); - assert_eq!(function_output, command_output); - } - - #[test] - fn test_get_message_on_stdin() { - // for the moment test against cat is not implemented - let command = vec!["wall"]; - let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) - .expect("External error"); - let pos_arg = matches.get_many(STRING).unwrap_or_default(); - let function_output = get_message(pos_arg).unwrap(); - assert_eq!(function_output, "Hello !\n"); - } - - #[test] - fn test_arguments_as_message() { - let command = vec!["wall", "Hello", "World", "!"]; - let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) - .expect("External error"); - let pos_arg = matches.get_many(STRING).unwrap_or_default(); - let function_output = get_message(pos_arg).unwrap(); - assert_eq!(function_output, "Hello World !"); - } - - #[test] - fn test_found_connected_users() { - let users = find_logged_users(); - assert_eq!( - users, - vec!( - OsString::from("/dev/tty2"), - OsString::from("/dev/pts/1"), - OsString::from("/dev/pts/2") - ) - ); - } - - #[test] - fn test_print_to_terminals() { - let users = find_logged_users(); - let _ = write_to_terminals(String::from("hello world!"), users); - let _ = write_to_terminals( - String::from("hello world!"), - vec![OsString::from("/dev/tty1")], - ); - } - - #[test] - fn test_get_sender() { - let sender = crate::get_sender(); - assert_eq!(sender, "pts/0"); - } -} diff --git a/src/uu/wall/wall.md b/src/uu/wall/wall.md new file mode 100644 index 00000000..1a100295 --- /dev/null +++ b/src/uu/wall/wall.md @@ -0,0 +1 @@ +wall - write message to all users diff --git a/tests/by-util/test_wall.rs b/tests/by-util/test_wall.rs new file mode 100644 index 00000000..c80deb48 --- /dev/null +++ b/tests/by-util/test_wall.rs @@ -0,0 +1,92 @@ +// 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 uutests::new_ucmd; +use std::ffi::OsString; +use std::process::{Command, Output}; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[test] +fn test_invalid_file() { + new_ucmd!().arg("not_existing_file.not_existing_extension").fails().code_is(1); +} + +// wall does not print the content of the file in the stdout, it sends it to the tty(s) +// Hence the use of cat to check if the get_message function can extract correctly the file + +// #[test] +// fn test_retreive_message_from_a_file() { +// let file = String::from("Cargo.toml"); +// let mut command = Command::new("cat"); +// command.arg(&file); +// let output: Output = command.output().expect("Failed to start 'cat' command"); +// assert!( +// output.status.success(), +// "'cat' command exit with failure status" +// ); +// let command_output = +// String::from_utf8(output.stdout).expect("Failed to convert 'cat'output"); + +// let command = vec!["wall", &file]; +// let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) +// .expect("External error"); +// let pos_arg = matches.get_many(STRING).unwrap_or_default(); +// let function_output = get_message(pos_arg).unwrap(); +// assert_eq!(function_output, command_output); +// } + +// #[test] +// fn test_get_message_on_stdin() { +// // Requires input by users +// let command = vec!["wall"]; +// let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) +// .expect("External error"); +// let pos_arg = matches.get_many(STRING).unwrap_or_default(); +// let function_output = get_message(pos_arg).unwrap(); +// assert_eq!(function_output, "Hello !\n"); +// } + +// #[test] +// fn test_arguments_as_message() { +// let command = vec!["wall", "Hello", "World", "!"]; +// let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) +// .expect("External error"); +// let pos_arg = matches.get_many(STRING).unwrap_or_default(); +// let function_output = get_message(pos_arg).unwrap(); +// assert_eq!(function_output, "Hello World !"); +// } + +// #[test] +// fn test_found_connected_users() { +// let users = find_logged_users(); +// assert_eq!( +// users, +// vec!( +// OsString::from("/dev/tty2"), +// OsString::from("/dev/pts/1"), +// OsString::from("/dev/pts/2") +// ) +// ); +// } + +// #[test] +// fn test_print_to_terminals() { +// let users = find_logged_users(); +// let _ = write_to_terminals(String::from("hello world!"), users); +// let _ = write_to_terminals( +// String::from("hello world!"), +// vec![OsString::from("/dev/tty1")], +// ); +// } + +// #[test] +// fn test_get_sender() { +// let sender = crate::get_sender(); +// assert_eq!(sender, "pts/0"); +// } diff --git a/tests/tests.rs b/tests/tests.rs index 09ffc245..89bace05 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -94,3 +94,7 @@ mod test_mcookie; #[cfg(feature = "uuidgen")] #[path = "by-util/test_uuidgen.rs"] mod test_uuidgen; + +#[cfg(feature = "wall")] +#[path = "by-util/test_wall.rs"] +mod test_wall; \ No newline at end of file From fc6850b771b98e209f4809def3184b3f2cd87d6f Mon Sep 17 00:00:00 2001 From: jbosslady Date: Mon, 27 Jul 2026 14:29:38 +0200 Subject: [PATCH 6/7] wall: spliting macos and linux usage version. introduction of global test into the library --- src/uu/wall/src/wall.rs | 19 +++++++++---------- tests/by-util/test_lslocks.rs | 4 +++- tests/by-util/test_setsid.rs | 2 +- tests/by-util/test_wall.rs | 7 +++++-- tests/tests.rs | 2 +- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index 30c20f42..1efb4d40 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -19,10 +19,10 @@ use thiserror::Error; #[cfg(unix)] use uucore::{ - error::{UError, UResult}, - format_usage, + error::{UError, UResult}, + format_usage, + translate, // unused at the moment... utmpx::Utmpx, - translate // unused at the moment... }; const STRING: &str = "string"; @@ -38,7 +38,6 @@ mod options { pub const VALID_LONG: &[&str] = &[OPT_GROUP]; } - #[cfg(target_os = "linux")] mod options { use super::{OPT_GROUP, OPT_NOBANNER, OPT_TIMEOUT}; @@ -71,12 +70,13 @@ impl UError for WallError { #[uucore::main(no_signals)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { #[cfg(windows)] - return Err(io::Error::new( - WallError::WindowsError)); + return Err(io::Error::new(WallError::WindowsError)); let args = args.skip(1).peekable(); match args_pre_scan(&args) { Ok(_) => {} - Err(e) => { return Err(WallError::ArgError); } + Err(e) => { + return Err(WallError::ArgError); + } } let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let message = get_message(matches.get_many(STRING).unwrap_or_default())?; @@ -90,8 +90,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let _matches: ArgMatches = uu_app().try_get_matches_from(args)?; Err(uucore::error::USimpleError::new( - 1, - "`wall` is available only on Unix platforms." + 1, + "`wall` is available only on Unix platforms.", )) } @@ -172,7 +172,6 @@ fn args_pre_scan(args: &ValuesRef) -> Result<(), String> { if arg == "--" { break; } - } Ok(()) } diff --git a/tests/by-util/test_lslocks.rs b/tests/by-util/test_lslocks.rs index a73d3d5f..f94a25aa 100644 --- a/tests/by-util/test_lslocks.rs +++ b/tests/by-util/test_lslocks.rs @@ -17,6 +17,8 @@ fn test_column_headers() { assert_eq!( cols, - ["COMMAND", "PID", "TYPE", "SIZE", "MODE", "M", "START", "END", "PATH"] + [ + "COMMAND", "PID", "TYPE", "SIZE", "MODE", "M", "START", "END", "PATH" + ] ); } diff --git a/tests/by-util/test_setsid.rs b/tests/by-util/test_setsid.rs index 002873f6..6d96492a 100644 --- a/tests/by-util/test_setsid.rs +++ b/tests/by-util/test_setsid.rs @@ -6,8 +6,8 @@ #[cfg(target_family = "unix")] mod unix { use uutests::new_ucmd; - use uutests::util::get_tests_binary; use uutests::util::UCommand; + use uutests::util::get_tests_binary; #[test] fn test_invalid_arg() { diff --git a/tests/by-util/test_wall.rs b/tests/by-util/test_wall.rs index c80deb48..aae4b4fa 100644 --- a/tests/by-util/test_wall.rs +++ b/tests/by-util/test_wall.rs @@ -3,9 +3,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use uutests::new_ucmd; use std::ffi::OsString; use std::process::{Command, Output}; +use uutests::new_ucmd; #[test] fn test_invalid_arg() { @@ -14,7 +14,10 @@ fn test_invalid_arg() { #[test] fn test_invalid_file() { - new_ucmd!().arg("not_existing_file.not_existing_extension").fails().code_is(1); + new_ucmd!() + .arg("not_existing_file.not_existing_extension") + .fails() + .code_is(1); } // wall does not print the content of the file in the stdout, it sends it to the tty(s) diff --git a/tests/tests.rs b/tests/tests.rs index 89bace05..d9bafecf 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -97,4 +97,4 @@ mod test_uuidgen; #[cfg(feature = "wall")] #[path = "by-util/test_wall.rs"] -mod test_wall; \ No newline at end of file +mod test_wall; From 782950bb2eabe27321c629e7f42501f707018dc2 Mon Sep 17 00:00:00 2001 From: JB on fedora Date: Tue, 28 Jul 2026 18:30:21 +0200 Subject: [PATCH 7/7] wall: fix windows compile error and add very basic test --- src/uu/wall/src/wall.rs | 108 +++++++++++++++++++------------------ tests/by-util/test_wall.rs | 92 ++++++------------------------- 2 files changed, 73 insertions(+), 127 deletions(-) diff --git a/src/uu/wall/src/wall.rs b/src/uu/wall/src/wall.rs index 1efb4d40..29de578a 100644 --- a/src/uu/wall/src/wall.rs +++ b/src/uu/wall/src/wall.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(not(unix))] +use clap::ArgMatches; use clap::builder::ValueParser; use clap::parser::ValuesRef; use clap::{Arg, ArgAction, Command}; @@ -18,46 +20,27 @@ use std::string::FromUtf8Error; use thiserror::Error; #[cfg(unix)] +use uucore::{error::USimpleError, utmpx::Utmpx}; use uucore::{ error::{UError, UResult}, - format_usage, - translate, // unused at the moment... - utmpx::Utmpx, + format_usage, translate, }; const STRING: &str = "string"; const OPT_GROUP: &str = "group"; +#[cfg(target_os = "linux")] const OPT_NOBANNER: &str = "nobanner"; -const OPT_TIMEOUT: &str = "timeout"; - -#[cfg(target_os = "macos")] -mod options { - use super::OPT_GROUP; // module don't automatically has access to const of parent - - pub const VALID_SHORT: &[char] = &['g']; - pub const VALID_LONG: &[&str] = &[OPT_GROUP]; -} - #[cfg(target_os = "linux")] -mod options { - use super::{OPT_GROUP, OPT_NOBANNER, OPT_TIMEOUT}; - - pub const VALID_SHORT: &[char] = &['g', 'n', 't']; - pub const VALID_LONG: &[&str] = &[OPT_GROUP, OPT_NOBANNER, OPT_TIMEOUT]; -} +const OPT_TIMEOUT: &str = "timeout"; #[derive(Error, Debug)] enum WallError { - #[error("wall: invalid argument")] - ArgError, #[error("wall: cannot read stdin")] Stdin(#[from] io::Error), #[error("wall: encoding error")] VecToString(#[from] FromUtf8Error), #[error("wall: osstring conversion failed")] ToStringError, - #[error("wall is not supported on windows")] - WindowsError, } impl UError for WallError { @@ -69,16 +52,9 @@ impl UError for WallError { #[cfg(target_family = "unix")] #[uucore::main(no_signals)] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - #[cfg(windows)] - return Err(io::Error::new(WallError::WindowsError)); - let args = args.skip(1).peekable(); - match args_pre_scan(&args) { - Ok(_) => {} - Err(e) => { - return Err(WallError::ArgError); - } - } - let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; + let matches = uucore::clap_localization::handle_clap_result(uu_app(), args) + .map_err(|e| USimpleError::new(1, e.to_string()))?; // Clap would have return 101 + // Might be considered wrong for --help and --version let message = get_message(matches.get_many(STRING).unwrap_or_default())?; let users = find_logged_users(); write_to_terminals(message, users)?; @@ -140,7 +116,7 @@ pub fn uu_app() -> Command { ) } -#[cfg(target_os = "macos")] +#[cfg(not(target_os = "linux"))] pub fn uu_app() -> Command { Command::new("wall") .version(uucore::crate_version!()) @@ -166,26 +142,26 @@ pub fn uu_app() -> Command { ) } -fn args_pre_scan(args: &ValuesRef) -> Result<(), String> { - for arg in args { - let arg = arg.to_string_lossy(); - if arg == "--" { - break; - } - } - Ok(()) -} - +#[cfg(target_family = "unix")] fn get_message(args: ValuesRef) -> Result { if args.len() == 0 { read_from_stdin() } else if args.len() == 1 { - read_from_file(args.into_iter().next().unwrap()) + match read_from_file(args.clone().next().unwrap()) { + Ok(str) => Ok(str), + Err(_e) => { + #[cfg(target_os = "linux")] + return concatenate_message(args); + #[cfg(not(target_os = "linux"))] + return Err(_e); + } + } } else { concatenate_message(args) } } +#[cfg(target_family = "unix")] fn read_from_stdin() -> Result { let mut buffer = Vec::new(); io::stdin().read_to_end(&mut buffer)?; @@ -193,6 +169,7 @@ fn read_from_stdin() -> Result { Ok(res) } +#[cfg(target_family = "unix")] fn read_from_file(file: &OsString) -> Result { let mut buffer = Vec::new(); let mut file = std::fs::File::open(file)?; @@ -201,6 +178,7 @@ fn read_from_file(file: &OsString) -> Result { Ok(res) } +#[cfg(target_family = "unix")] fn concatenate_message(args: ValuesRef) -> Result { let mut res = String::new(); for arg in args { @@ -211,6 +189,7 @@ fn concatenate_message(args: ValuesRef) -> Result { Ok(res) } +#[cfg(target_family = "unix")] fn find_logged_users() -> Vec { let mut res = Vec::::new(); for ut in Utmpx::iter_all_records() { @@ -223,6 +202,7 @@ fn find_logged_users() -> Vec { res } +#[cfg(target_family = "unix")] fn wall_intro_message() -> String { let user = "USER"; let biding = unistd::gethostname().unwrap_or_else(|_| "".into()); @@ -233,16 +213,31 @@ fn wall_intro_message() -> String { let tty = &get_sender(); let datetime = get_hour_and_date(); - format!( - "\r\nBroadcast message from {}@{} ({tty}) at ({datetime}) \r\n\r\n", + #[cfg(target_os = "macos")] + return format!( + "\r\nBroadcast message from {}@{} ({tty}) at ({datetime} \r\n\r\n", user.to_string_lossy(), hostname - ) + ); + #[cfg(target_os = "linux")] + return format!( + "\r\nBroadcast message from {}@{} ({tty}) ({datetime}) \r\n\r\n", + user.to_string_lossy(), + hostname + ); } +#[cfg(target_family = "unix")] fn write_to_terminals(message: String, users: Vec) -> UResult<()> { - let format_message = message.replace("\n", "\r\n\n"); - let transmission = wall_intro_message() + &format_message; + #[cfg(target_os = "linux")] + let mut formatted_message = message.replace('\n', "\r\n\n"); + #[cfg(target_os = "linux")] + formatted_message.push_str("\r\n\n"); + + #[cfg(not(target_os = "linux"))] + let formatted_message = message.replace('\n', "\r\n\n"); + + let transmission = format!("{}{}", wall_intro_message(), formatted_message); for user in users { let mut file = match std::fs::OpenOptions::new().write(true).open(user) { Ok(f) => f, @@ -261,7 +256,7 @@ fn write_to_terminals(message: String, users: Vec) -> UResult<()> { #[cfg(target_os = "linux")] fn get_hour_and_date() -> String { - chrono::Local::now().format("%a %b %e %H:%M %Z").to_string() + chrono::Local::now().format("%a %b %e %H:%M %Y").to_string() } #[cfg(target_os = "macos")] @@ -269,9 +264,20 @@ fn get_hour_and_date() -> String { chrono::Local::now().format("%a %b %e %H:%M %Z").to_string() } +#[cfg(target_os = "macos")] +fn get_sender() -> String { + unistd::ttyname(std::io::stdin().as_fd()) + .unwrap_or_else(|_| "".into()) + .to_string_lossy() + .to_string() +} + +#[cfg(target_os = "linux")] fn get_sender() -> String { unistd::ttyname(std::io::stdin().as_fd()) .unwrap_or_else(|_| "".into()) .to_string_lossy() + .strip_prefix("/dev/") // Wall doesn't print /dev/ after tty name, but might not be the way it does it + .unwrap_or("") .to_string() } diff --git a/tests/by-util/test_wall.rs b/tests/by-util/test_wall.rs index aae4b4fa..94b1a613 100644 --- a/tests/by-util/test_wall.rs +++ b/tests/by-util/test_wall.rs @@ -3,93 +3,33 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use std::ffi::OsString; -use std::process::{Command, Output}; use uutests::new_ucmd; +#[cfg(target_family = "unix")] #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } +#[cfg(not(target_family = "unix"))] +#[test] +fn unsupported_feature() { + new_ucmd!().arg("Cargo.toml").fails().code_is(1); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_invalid_file() { + new_ucmd!().arg("not_existing_file.not_existing_extension"); // Should print non-file name as broadcast +} + +#[cfg(target_os = "macos")] #[test] fn test_invalid_file() { new_ucmd!() .arg("not_existing_file.not_existing_extension") .fails() - .code_is(1); + .code_is(1); // On macOS, file not existing is an error } -// wall does not print the content of the file in the stdout, it sends it to the tty(s) -// Hence the use of cat to check if the get_message function can extract correctly the file - -// #[test] -// fn test_retreive_message_from_a_file() { -// let file = String::from("Cargo.toml"); -// let mut command = Command::new("cat"); -// command.arg(&file); -// let output: Output = command.output().expect("Failed to start 'cat' command"); -// assert!( -// output.status.success(), -// "'cat' command exit with failure status" -// ); -// let command_output = -// String::from_utf8(output.stdout).expect("Failed to convert 'cat'output"); - -// let command = vec!["wall", &file]; -// let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) -// .expect("External error"); -// let pos_arg = matches.get_many(STRING).unwrap_or_default(); -// let function_output = get_message(pos_arg).unwrap(); -// assert_eq!(function_output, command_output); -// } - -// #[test] -// fn test_get_message_on_stdin() { -// // Requires input by users -// let command = vec!["wall"]; -// let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) -// .expect("External error"); -// let pos_arg = matches.get_many(STRING).unwrap_or_default(); -// let function_output = get_message(pos_arg).unwrap(); -// assert_eq!(function_output, "Hello !\n"); -// } - -// #[test] -// fn test_arguments_as_message() { -// let command = vec!["wall", "Hello", "World", "!"]; -// let matches = uucore::clap_localization::handle_clap_result(uu_app(), command) -// .expect("External error"); -// let pos_arg = matches.get_many(STRING).unwrap_or_default(); -// let function_output = get_message(pos_arg).unwrap(); -// assert_eq!(function_output, "Hello World !"); -// } - -// #[test] -// fn test_found_connected_users() { -// let users = find_logged_users(); -// assert_eq!( -// users, -// vec!( -// OsString::from("/dev/tty2"), -// OsString::from("/dev/pts/1"), -// OsString::from("/dev/pts/2") -// ) -// ); -// } - -// #[test] -// fn test_print_to_terminals() { -// let users = find_logged_users(); -// let _ = write_to_terminals(String::from("hello world!"), users); -// let _ = write_to_terminals( -// String::from("hello world!"), -// vec![OsString::from("/dev/tty1")], -// ); -// } - -// #[test] -// fn test_get_sender() { -// let sender = crate::get_sender(); -// assert_eq!(sender, "pts/0"); -// } +// wall does not print the content of the file in the stdout, it sends it to the tty(s).