From 8ef22a0811a9f4c0f7797ee915674d083913ef98 Mon Sep 17 00:00:00 2001 From: jbosslady Date: Tue, 23 Jun 2026 02:16:55 +0200 Subject: [PATCH 1/4] 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/4] 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/4] 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/4] 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};