From baf273da9867d5969416a36b0f772198d5535369 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Sat, 9 Mar 2024 22:54:17 +0900 Subject: [PATCH 1/5] System::times --- yash-env/src/system.rs | 30 ++++++++++++++++++++++++++++++ yash-env/src/system/real.rs | 25 +++++++++++++++++++++++++ yash-env/src/system/virtual.rs | 19 ++++++++++++++----- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/yash-env/src/system.rs b/yash-env/src/system.rs index 78ab7477..1fdd7c88 100644 --- a/yash-env/src/system.rs +++ b/yash-env/src/system.rs @@ -196,6 +196,9 @@ pub trait System: Debug { #[must_use] fn now(&self) -> Instant; + /// Returns consumed CPU times. + fn times(&self) -> nix::Result; + /// Gets and/or sets the signal blocking mask. /// /// This is a low-level function used internally by @@ -397,6 +400,30 @@ pub trait System: Debug { /// [`System::fstatat`]. pub const AT_FDCWD: Fd = Fd(nix::libc::AT_FDCWD); +/// Set of consumed CPU time +/// +/// This structure contains four CPU time values, all in clock ticks. To convert +/// them to seconds, divide each value by the number of clock ticks per second +/// (`ticks_per_second`). +/// +/// This structure is returned by [`System::times`]. +#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +pub struct Times { + /// User CPU time consumed by the current process + pub self_user: u64, + /// System CPU time consumed by the current process + pub self_system: u64, + /// User CPU time consumed by the children of the current process + pub children_user: u64, + /// System CPU time consumed by the children of the current process + pub children_system: u64, + + /// Number of clock ticks per second + /// + /// This value is used to convert the consumed CPU time to seconds. + pub ticks_per_second: u64, +} + /// How to handle a signal. #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)] pub enum SignalHandling { @@ -883,6 +910,9 @@ impl System for SharedSystem { fn now(&self) -> Instant { self.0.borrow().now() } + fn times(&self) -> nix::Result { + self.0.borrow().times() + } fn sigmask( &mut self, how: SigmaskHow, diff --git a/yash-env/src/system/real.rs b/yash-env/src/system/real.rs index ecb27aef..78fef092 100644 --- a/yash-env/src/system/real.rs +++ b/yash-env/src/system/real.rs @@ -33,6 +33,7 @@ use super::SigmaskHow; use super::Signal; use super::System; use super::TimeSpec; +use super::Times; use crate::io::Fd; use crate::job::Pid; use crate::job::ProcessState; @@ -54,6 +55,7 @@ use std::ffi::OsStr; use std::ffi::OsString; use std::future::Future; use std::io::SeekFrom; +use std::mem::MaybeUninit; use std::os::unix::ffi::OsStrExt; use std::os::unix::io::IntoRawFd; use std::path::Path; @@ -260,6 +262,29 @@ impl System for RealSystem { Instant::now() } + fn times(&self) -> nix::Result { + let mut tms = MaybeUninit::::uninit(); + let raw_result = unsafe { nix::libc::times(tms.as_mut_ptr()) }; + if raw_result == -1 { + return Err(Errno::last()); + } + let tms = unsafe { tms.assume_init() }; + + let ticks_per_second = unsafe { nix::libc::sysconf(nix::libc::_SC_CLK_TCK) }; + if ticks_per_second <= 0 { + return Err(Errno::last()); + } + let ticks_per_second: u64 = ticks_per_second.try_into().map_err(|_| Errno::EOVERFLOW)?; + + Ok(Times { + self_user: tms.tms_utime.try_into().map_err(|_| Errno::EOVERFLOW)?, + self_system: tms.tms_stime.try_into().map_err(|_| Errno::EOVERFLOW)?, + children_user: tms.tms_cutime.try_into().map_err(|_| Errno::EOVERFLOW)?, + children_system: tms.tms_cstime.try_into().map_err(|_| Errno::EOVERFLOW)?, + ticks_per_second, + }) + } + fn sigmask( &mut self, how: SigmaskHow, diff --git a/yash-env/src/system/virtual.rs b/yash-env/src/system/virtual.rs index 492da9e8..b43e37f0 100644 --- a/yash-env/src/system/virtual.rs +++ b/yash-env/src/system/virtual.rs @@ -65,6 +65,7 @@ use super::SigSet; use super::SigmaskHow; use super::Signal; use super::TimeSpec; +use super::Times; use super::AT_FDCWD; use crate::io::Fd; use crate::job::Pid; @@ -615,6 +616,11 @@ impl System for VirtualSystem { .expect("SystemState::now not assigned") } + /// Returns `times` in [`SystemState`]. + fn times(&self) -> nix::Result { + Ok(self.state.borrow().times) + } + fn sigmask( &mut self, how: SigmaskHow, @@ -1042,16 +1048,19 @@ fn raise_sigchld(state: &mut SystemState, target_pid: Pid) { /// State of the virtual system. #[derive(Clone, Debug, Default)] pub struct SystemState { - /// Current time. + /// Current time pub now: Option, - /// Task manager that can execute asynchronous tasks. + /// Consumed CPU time + pub times: Times, + + /// Task manager that can execute asynchronous tasks /// /// The virtual system uses this executor to run (virtual) child processes. /// If `executor` is `None`, [`VirtualSystem::new_child_process`] will fail. pub executor: Option>, - /// Processes running in the system. + /// Processes running in the system pub processes: BTreeMap, /// Process group ID of the foreground process group @@ -1061,10 +1070,10 @@ pub struct SystemState { /// more _correct_ implementation in the future. pub foreground: Option, - /// Collection of files existing in the virtual system. + /// Collection of files existing in the virtual system pub file_system: FileSystem, - /// Map from user names to their home directory paths. + /// Map from user names to their home directory paths /// /// [`VirtualSystem::getpwnam_dir`] looks up its argument in this /// dictionary. From 5398390f5ff5f32beac9d409442795e739f1a136 Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Sat, 9 Mar 2024 13:14:52 +0900 Subject: [PATCH 2/5] Document times built-in --- yash-builtin/src/lib.rs | 8 ++++ yash-builtin/src/times.rs | 79 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 yash-builtin/src/times.rs diff --git a/yash-builtin/src/lib.rs b/yash-builtin/src/lib.rs index bec64052..bb3e8f37 100644 --- a/yash-builtin/src/lib.rs +++ b/yash-builtin/src/lib.rs @@ -75,6 +75,7 @@ pub mod set; pub mod shift; #[cfg(feature = "yash-semantics")] pub mod source; +pub mod times; pub mod trap; #[cfg(feature = "yash-semantics")] pub mod r#type; @@ -265,6 +266,13 @@ pub const BUILTINS: &[(&str, Builtin)] = &[ execute: |env, args| Box::pin(source::main(env, args)), }, ), + ( + "times", + Builtin { + r#type: Special, + execute: |env, args| Box::pin(times::main(env, args)), + }, + ), ( "trap", Builtin { diff --git a/yash-builtin/src/times.rs b/yash-builtin/src/times.rs new file mode 100644 index 00000000..20e04c45 --- /dev/null +++ b/yash-builtin/src/times.rs @@ -0,0 +1,79 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2024 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Times built-in +//! +//! The **`times`** built-in is used to display the accumulated user and system +//! times for the shell and its children. +//! +//! # Synopsis +//! +//! ```sh +//! times +//! ``` +//! +//! # Description +//! +//! The built-in prints the accumulated user and system times for the shell and +//! its children. +//! +//! # Options +//! +//! None. +//! +//! # Operands +//! +//! None. +//! +//! # Standard output +//! +//! Two lines are printed to the standard output, each in the following format: +//! +//! ```text +//! 1m2.345678s 3m4.567890s +//! ``` +//! +//! The first field of each line is the user time, and the second field is the +//! system time. +//! The first line shows the times consumed by the shell itself, and the +//! second line shows the times consumed by its children. +//! +//! # Errors +//! +//! It is an error if the times cannot be obtained or the standard output is not +//! writable. +//! +//! # Exit status +//! +//! Zero unless an error occurred. +//! +//! # Portability +//! +//! The `times` built-in is defined in POSIX. +//! +//! POSIX requires each field to be printed with six digits after the decimal +//! point, but many implementations print less. Note that the number of digits +//! does not necessarily indicate the precision of the times. + +use yash_env::semantics::Field; +use yash_env::Env; + +/// Entry point of the `times` built-in +pub async fn main(env: &mut Env, args: Vec) -> crate::Result { + _ = env; + _ = args; + todo!() +} From 9ba0755997a16390ba18bbde8748002b5682312c Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Sun, 10 Mar 2024 23:27:08 +0900 Subject: [PATCH 3/5] Behavior of times built-in --- yash-builtin/src/times.rs | 22 ++++++- yash-builtin/src/times/format.rs | 109 +++++++++++++++++++++++++++++++ yash-builtin/src/times/syntax.rs | 67 +++++++++++++++++++ 3 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 yash-builtin/src/times/format.rs create mode 100644 yash-builtin/src/times/syntax.rs diff --git a/yash-builtin/src/times.rs b/yash-builtin/src/times.rs index 20e04c45..51c0e982 100644 --- a/yash-builtin/src/times.rs +++ b/yash-builtin/src/times.rs @@ -68,12 +68,28 @@ //! point, but many implementations print less. Note that the number of digits //! does not necessarily indicate the precision of the times. +use crate::common::output; +use crate::common::report_error; +use crate::common::report_simple_failure; use yash_env::semantics::Field; use yash_env::Env; +use yash_env::System; + +mod format; +mod syntax; /// Entry point of the `times` built-in pub async fn main(env: &mut Env, args: Vec) -> crate::Result { - _ = env; - _ = args; - todo!() + match syntax::parse(env, args) { + Ok(()) => match env.system.times() { + Ok(times) => { + let result = format::format(×); + output(env, &result).await + } + Err(error) => { + report_simple_failure(env, &format!("cannot obtain times: {error}")).await + } + }, + Err(error) => report_error(env, &error).await, + } } diff --git a/yash-builtin/src/times/format.rs b/yash-builtin/src/times/format.rs new file mode 100644 index 00000000..db4b21be --- /dev/null +++ b/yash-builtin/src/times/format.rs @@ -0,0 +1,109 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2024 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Formatting the result of the times built-in + +use yash_env::system::Times; + +/// Formats a single time. +/// +/// This function panics if the `ticks_per_second` is zero. +fn format_one_time(ticks: u64, ticks_per_second: u64, result: &mut W) -> std::fmt::Result +where + W: std::fmt::Write, +{ + let seconds = ticks / ticks_per_second; + let minutes = seconds / 60; + let sub_minute_ticks = ticks - minutes * 60 * ticks_per_second; + let seconds = sub_minute_ticks as f64 / ticks_per_second as f64; + write!(result, "{minutes}m{seconds:.6}s") +} + +/// Formats the result of the times built-in. +/// +/// This function takes a `Times` structure and returns a string that is to be +/// printed to the standard output. See the +/// [parent module documentation](crate::times) for the format. +pub fn format(times: &Times) -> String { + let mut result = String::with_capacity(64); + + format_one_time(times.self_user, times.ticks_per_second, &mut result).unwrap(); + result.push(' '); + format_one_time(times.self_system, times.ticks_per_second, &mut result).unwrap(); + result.push('\n'); + format_one_time(times.children_user, times.ticks_per_second, &mut result).unwrap(); + result.push(' '); + format_one_time(times.children_system, times.ticks_per_second, &mut result).unwrap(); + result.push('\n'); + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_one_time_zero() { + let mut result = String::new(); + format_one_time(0, 100, &mut result).unwrap(); + assert_eq!(result, "0m0.000000s"); + } + + #[test] + fn format_one_time_less_than_one_second() { + let mut result = String::new(); + format_one_time(50, 100, &mut result).unwrap(); + assert_eq!(result, "0m0.500000s"); + } + + #[test] + fn format_one_time_one_second() { + let mut result = String::new(); + format_one_time(1000, 1000, &mut result).unwrap(); + assert_eq!(result, "0m1.000000s"); + } + + #[test] + fn format_one_time_more_than_one_second() { + let mut result = String::new(); + format_one_time(1225, 100, &mut result).unwrap(); + assert_eq!(result, "0m12.250000s"); + } + + #[test] + fn format_one_time_more_than_one_minute() { + let mut result = String::new(); + format_one_time(123450, 100, &mut result).unwrap(); + assert_eq!(result, "20m34.500000s"); + } + + #[test] + fn format_times() { + let times = Times { + self_user: 1250, + self_system: 6525, + children_user: 2475, + children_system: 60000, + ticks_per_second: 100, + }; + let result = format(×); + assert_eq!( + result, + "0m12.500000s 1m5.250000s\n0m24.750000s 10m0.000000s\n" + ); + } +} diff --git a/yash-builtin/src/times/syntax.rs b/yash-builtin/src/times/syntax.rs new file mode 100644 index 00000000..014173a9 --- /dev/null +++ b/yash-builtin/src/times/syntax.rs @@ -0,0 +1,67 @@ +// This file is part of yash, an extended POSIX shell. +// Copyright (C) 2024 WATANABE Yuki +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Command line syntax parsing for the times built-in + +use crate::common::syntax::{parse_arguments, Mode}; +use std::borrow::Cow; +use thiserror::Error; +use yash_env::semantics::Field; +use yash_env::Env; +use yash_syntax::source::pretty::{Annotation, AnnotationType, MessageBase}; + +/// Error in parsing command line arguments +#[derive(Clone, Debug, Eq, Error, PartialEq)] +#[non_exhaustive] +pub enum Error { + /// An error occurred in the common parser. + #[error(transparent)] + CommonError(#[from] crate::common::syntax::ParseError<'static>), + + /// One or more operands are given. + #[error("unexpected operand")] + UnexpectedOperands(Vec), +} + +impl MessageBase for Error { + fn message_title(&self) -> Cow { + self.to_string().into() + } + + fn main_annotation(&self) -> Annotation<'_> { + use Error::*; + match self { + CommonError(e) => e.main_annotation(), + UnexpectedOperands(operands) => Annotation::new( + AnnotationType::Error, + format!("{}: unexpected operand", operands[0].value).into(), + &operands[0].origin, + ), + } + } +} + +/// Parses command line arguments for the times built-in. +pub fn parse(env: &Env, args: Vec) -> Result<(), Error> { + let (options, operands) = parse_arguments(&[], Mode::with_env(env), args)?; + debug_assert_eq!(options, []); + + if operands.is_empty() { + Ok(()) + } else { + Err(Error::UnexpectedOperands(operands)) + } +} From aa894ce25ff4c74d7d7765cc5907d855733db48b Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Mon, 11 Mar 2024 00:50:40 +0900 Subject: [PATCH 4/5] Retain Times values in f64 This change makes the `Times` struct contain seconds in f64 instead of clock ticks in u64. The `tms` struct returned by the underlying `times` function contains values in `clock_t`. The previous implementation assumed that it is an integral type, but POSIX allows it to be a floating-point type. This change makes the `Times` struct use f64 for all time values, which can be cast from any types `clock_t` may be. --- yash-builtin/src/times/format.rs | 54 +++++++++++++++++++------------- yash-env/src/system.rs | 19 ++++------- yash-env/src/system/real.rs | 12 +++---- 3 files changed, 43 insertions(+), 42 deletions(-) diff --git a/yash-builtin/src/times/format.rs b/yash-builtin/src/times/format.rs index db4b21be..e8a12a84 100644 --- a/yash-builtin/src/times/format.rs +++ b/yash-builtin/src/times/format.rs @@ -19,17 +19,17 @@ use yash_env::system::Times; /// Formats a single time. -/// -/// This function panics if the `ticks_per_second` is zero. -fn format_one_time(ticks: u64, ticks_per_second: u64, result: &mut W) -> std::fmt::Result +fn format_one_time(seconds: f64, result: &mut W) -> std::fmt::Result where W: std::fmt::Write, { - let seconds = ticks / ticks_per_second; - let minutes = seconds / 60; - let sub_minute_ticks = ticks - minutes * 60 * ticks_per_second; - let seconds = sub_minute_ticks as f64 / ticks_per_second as f64; - write!(result, "{minutes}m{seconds:.6}s") + // Make sure the seconds are rounded to 6 decimal places. Without this, the + // result may be something like "0m60.000000s" instead of "1m0.000000s". + let seconds = (seconds * 1000000.0).round() / 1000000.0; + + let minutes = seconds.div_euclid(60.0); + let sub_minute_seconds = seconds.rem_euclid(60.0); + write!(result, "{minutes:.0}m{sub_minute_seconds:.6}s") } /// Formats the result of the times built-in. @@ -40,13 +40,13 @@ where pub fn format(times: &Times) -> String { let mut result = String::with_capacity(64); - format_one_time(times.self_user, times.ticks_per_second, &mut result).unwrap(); + format_one_time(times.self_user, &mut result).unwrap(); result.push(' '); - format_one_time(times.self_system, times.ticks_per_second, &mut result).unwrap(); + format_one_time(times.self_system, &mut result).unwrap(); result.push('\n'); - format_one_time(times.children_user, times.ticks_per_second, &mut result).unwrap(); + format_one_time(times.children_user, &mut result).unwrap(); result.push(' '); - format_one_time(times.children_system, times.ticks_per_second, &mut result).unwrap(); + format_one_time(times.children_system, &mut result).unwrap(); result.push('\n'); result @@ -59,46 +59,56 @@ mod tests { #[test] fn format_one_time_zero() { let mut result = String::new(); - format_one_time(0, 100, &mut result).unwrap(); + format_one_time(0.0, &mut result).unwrap(); assert_eq!(result, "0m0.000000s"); } #[test] fn format_one_time_less_than_one_second() { let mut result = String::new(); - format_one_time(50, 100, &mut result).unwrap(); + format_one_time(0.5, &mut result).unwrap(); assert_eq!(result, "0m0.500000s"); } #[test] fn format_one_time_one_second() { let mut result = String::new(); - format_one_time(1000, 1000, &mut result).unwrap(); + format_one_time(1.0, &mut result).unwrap(); assert_eq!(result, "0m1.000000s"); } #[test] fn format_one_time_more_than_one_second() { let mut result = String::new(); - format_one_time(1225, 100, &mut result).unwrap(); + format_one_time(12.25, &mut result).unwrap(); assert_eq!(result, "0m12.250000s"); } #[test] fn format_one_time_more_than_one_minute() { let mut result = String::new(); - format_one_time(123450, 100, &mut result).unwrap(); + format_one_time(1234.50, &mut result).unwrap(); assert_eq!(result, "20m34.500000s"); } + #[test] + fn format_one_time_almost_one_minute() { + let mut result = String::new(); + format_one_time(59.9999990, &mut result).unwrap(); + assert_eq!(result, "0m59.999999s"); + + let mut result = String::new(); + format_one_time(59.9999999, &mut result).unwrap(); + assert_eq!(result, "1m0.000000s"); + } + #[test] fn format_times() { let times = Times { - self_user: 1250, - self_system: 6525, - children_user: 2475, - children_system: 60000, - ticks_per_second: 100, + self_user: 12.5, + self_system: 65.25, + children_user: 24.75, + children_system: 600.0, }; let result = format(×); assert_eq!( diff --git a/yash-env/src/system.rs b/yash-env/src/system.rs index 1fdd7c88..18eabfde 100644 --- a/yash-env/src/system.rs +++ b/yash-env/src/system.rs @@ -402,26 +402,19 @@ pub const AT_FDCWD: Fd = Fd(nix::libc::AT_FDCWD); /// Set of consumed CPU time /// -/// This structure contains four CPU time values, all in clock ticks. To convert -/// them to seconds, divide each value by the number of clock ticks per second -/// (`ticks_per_second`). +/// This structure contains four CPU time values, all in seconds. /// /// This structure is returned by [`System::times`]. -#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Times { /// User CPU time consumed by the current process - pub self_user: u64, + pub self_user: f64, /// System CPU time consumed by the current process - pub self_system: u64, + pub self_system: f64, /// User CPU time consumed by the children of the current process - pub children_user: u64, + pub children_user: f64, /// System CPU time consumed by the children of the current process - pub children_system: u64, - - /// Number of clock ticks per second - /// - /// This value is used to convert the consumed CPU time to seconds. - pub ticks_per_second: u64, + pub children_system: f64, } /// How to handle a signal. diff --git a/yash-env/src/system/real.rs b/yash-env/src/system/real.rs index 78fef092..0c56bbe7 100644 --- a/yash-env/src/system/real.rs +++ b/yash-env/src/system/real.rs @@ -265,7 +265,7 @@ impl System for RealSystem { fn times(&self) -> nix::Result { let mut tms = MaybeUninit::::uninit(); let raw_result = unsafe { nix::libc::times(tms.as_mut_ptr()) }; - if raw_result == -1 { + if raw_result == (-1) as _ { return Err(Errno::last()); } let tms = unsafe { tms.assume_init() }; @@ -274,14 +274,12 @@ impl System for RealSystem { if ticks_per_second <= 0 { return Err(Errno::last()); } - let ticks_per_second: u64 = ticks_per_second.try_into().map_err(|_| Errno::EOVERFLOW)?; Ok(Times { - self_user: tms.tms_utime.try_into().map_err(|_| Errno::EOVERFLOW)?, - self_system: tms.tms_stime.try_into().map_err(|_| Errno::EOVERFLOW)?, - children_user: tms.tms_cutime.try_into().map_err(|_| Errno::EOVERFLOW)?, - children_system: tms.tms_cstime.try_into().map_err(|_| Errno::EOVERFLOW)?, - ticks_per_second, + self_user: tms.tms_utime as f64 / ticks_per_second as f64, + self_system: tms.tms_stime as f64 / ticks_per_second as f64, + children_user: tms.tms_cutime as f64 / ticks_per_second as f64, + children_system: tms.tms_cstime as f64 / ticks_per_second as f64, }) } From 907f500ee763be3ef58c91c6f50a7f3ce4dc61ac Mon Sep 17 00:00:00 2001 From: WATANABE Yuki Date: Mon, 11 Mar 2024 01:03:41 +0900 Subject: [PATCH 5/5] Comment --- yash-builtin/src/times/format.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/yash-builtin/src/times/format.rs b/yash-builtin/src/times/format.rs index e8a12a84..d1eb1c6f 100644 --- a/yash-builtin/src/times/format.rs +++ b/yash-builtin/src/times/format.rs @@ -40,6 +40,7 @@ where pub fn format(times: &Times) -> String { let mut result = String::with_capacity(64); + // The Write impl for String never returns an error, so unwrap is safe here. format_one_time(times.self_user, &mut result).unwrap(); result.push(' '); format_one_time(times.self_system, &mut result).unwrap();