diff --git a/README.md b/README.md index d83fea403a..5368481de6 100644 --- a/README.md +++ b/README.md @@ -4299,6 +4299,20 @@ $ just foo hello ``` +Which may include a format string: + +```just +[timestamp('%H:%M:%S%.3f')] +foo: + echo hello +``` + +``` +$ just foo +[07:28:46.487] echo hello +hello +``` + ### Signal Handling [Signals](https://en.wikipedia.org/wiki/Signal_(IPC)) are messages sent to @@ -4758,6 +4772,7 @@ change their behavior. | `[script(COMMAND)]`1.32.0 | recipe | Execute recipe as a script interpreted by `COMMAND`. See [script recipes](#script-recipes) for more details. | | `[script]`1.33.0 | recipe | Execute recipe as script. See [script recipes](#script-recipes) for more details. | | `[shell]`1.52.0 | recipe | Execute recipe as a shell recipe, overriding `set default-script`. | +| `[timestamp(FORMAT)]`master | recipe | Print command timestamps with format `FORMAT`. `FORMAT` may be an expression. | | `[timestamp]`master | recipe | Print command timestamps. | | `[unix]`1.8.0 | any1.56.0 | Enable item on unixes. (Includes macOS). | | `[windows]`1.8.0 | any1.56.0 | Enable item on Windows. | diff --git a/src/attribute.rs b/src/attribute.rs index a9f5cbc88c..c88b2a3d7b 100644 --- a/src/attribute.rs +++ b/src/attribute.rs @@ -65,7 +65,7 @@ pub(crate) enum Attribute<'src> { Private, Script(Option>>), Shell, - Timestamp, + Timestamp(Option>), Unix, Windows, WorkingDirectory(Expression<'src>), @@ -75,7 +75,7 @@ impl AttributeKind { fn accepts_expressions(self) -> bool { matches!( self, - Self::Confirm | Self::Doc | Self::Env | Self::WorkingDirectory + Self::Confirm | Self::Doc | Self::Env | Self::Timestamp | Self::WorkingDirectory ) } @@ -117,10 +117,9 @@ impl AttributeKind { | Self::PositionalArguments | Self::Private | Self::Shell - | Self::Timestamp | Self::Unix | Self::Windows => 0..=0, - Self::Confirm | Self::Doc => 0..=1, + Self::Confirm | Self::Doc | Self::Timestamp => 0..=1, Self::Continue | Self::Script => 0..=usize::MAX, Self::Arg | Self::Extension | Self::Group | Self::WorkingDirectory => 1..=1, Self::Env => 2..=2, @@ -203,6 +202,9 @@ impl<'src> Attribute<'src> { let (_, value) = arguments.next().unwrap(); Ok(Self::Env(key, value)) } + AttributeKind::Timestamp => Ok(Self::Timestamp( + arguments.into_iter().next().map(|(_, expr)| expr), + )), AttributeKind::WorkingDirectory => Ok(Self::WorkingDirectory( arguments.into_iter().next().map(|(_, expr)| expr).unwrap(), )), @@ -248,6 +250,7 @@ impl<'src> Attribute<'src> { AttributeKind::Confirm | AttributeKind::Doc | AttributeKind::Env + | AttributeKind::Timestamp | AttributeKind::WorkingDirectory => { unreachable!() } @@ -276,7 +279,6 @@ impl<'src> Attribute<'src> { }) }), AttributeKind::Shell => Self::Shell, - AttributeKind::Timestamp => Self::Timestamp, AttributeKind::Unix => Self::Unix, AttributeKind::Windows => Self::Windows, }; @@ -596,7 +598,7 @@ impl Display for Attribute<'_> { | Self::Private | Self::Script(None) | Self::Shell - | Self::Timestamp + | Self::Timestamp(None) | Self::Unix | Self::Windows => {} Self::Cache { @@ -620,6 +622,7 @@ impl Display for Attribute<'_> { } Self::Confirm(Some(argument)) | Self::Doc(Some(argument)) + | Self::Timestamp(Some(argument)) | Self::WorkingDirectory(argument) => { write!(f, "({argument})")?; } diff --git a/src/recipe.rs b/src/recipe.rs index 060b9a1b16..b0238821a3 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -201,13 +201,32 @@ impl<'src> Recipe<'src> { self.attributes.contains(AttributeKind::NoQuiet) } - fn timestamp(&self, config: &Config) -> RunResult<'static, Option> { - (config.timestamp || self.attributes.contains(AttributeKind::Timestamp)) - .then(|| { - datetime_format(chrono::Local::now(), &config.timestamp_format) - .map_err(Error::DatetimeFormat) - }) - .transpose() + fn timestamp_format( + &self, + config: &Config, + evaluator: &mut Evaluator<'src, '_>, + ) -> RunResult<'src, Option> { + if let Some(attribute) = self.attributes.get(AttributeKind::Timestamp) { + let Attribute::Timestamp(format) = attribute else { + unreachable!(); + }; + Ok(Some( + format + .as_ref() + .map(|expression| { + evaluator.evaluate_string( + expression, + StringContext::TimestampAttribute(self.attributes.name(attribute)), + ) + }) + .transpose()? + .unwrap_or_else(|| config.timestamp_format.clone()), + )) + } else if config.timestamp { + Ok(Some(config.timestamp_format.clone())) + } else { + Ok(None) + } } pub(crate) fn run<'run>( @@ -286,6 +305,8 @@ impl<'src> Recipe<'src> { let working_directory = self.working_directory(context, &mut evaluator)?; + let timestamp_format = self.timestamp_format(config, &mut evaluator)?; + loop { let Some(line) = lines.peek() else { return Ok(()); @@ -330,7 +351,10 @@ impl<'src> Recipe<'src> { let infallible = sigils.contains(&Sigil::Infallible); let quiet = sigils.contains(&Sigil::Quiet); - let timestamp = self.timestamp(config)?; + let timestamp = timestamp_format + .as_deref() + .map(|format| datetime_format(chrono::Local::now(), format).map_err(Error::DatetimeFormat)) + .transpose()?; if config.dry_run || config.verbosity.loquacious() @@ -448,7 +472,10 @@ impl<'src> Recipe<'src> { ) -> RunResult<'src> { let config = &context.config; - if let Some(timestamp) = self.timestamp(config)? { + if let Some(format) = self.timestamp_format(config, &mut evaluator)? { + let timestamp = + datetime_format(chrono::Local::now(), &format).map_err(Error::DatetimeFormat)?; + let color = if config.highlight { config.color.command(config.command_color) } else { diff --git a/src/string_context.rs b/src/string_context.rs index f144894d46..56a1b2f8b9 100644 --- a/src/string_context.rs +++ b/src/string_context.rs @@ -5,6 +5,7 @@ pub(crate) enum StringContext<'src> { EnvKey(Name<'src>), Function(Name<'src>), Setting(Name<'src>), + TimestampAttribute(Name<'src>), WorkingDirectoryAttribute(Name<'src>), } @@ -14,6 +15,7 @@ impl<'src> StringContext<'src> { Self::EnvKey(name) | Self::Function(name) | Self::Setting(name) + | Self::TimestampAttribute(name) | Self::WorkingDirectoryAttribute(name) => name.token, } } @@ -25,6 +27,9 @@ impl Display for StringContext<'_> { Self::EnvKey(_) => write!(f, "used as `env` attribute name"), Self::Function(name) => write!(f, "passed to `{name}()`"), Self::Setting(name) => write!(f, "assigned to `{name}` setting"), + Self::TimestampAttribute(_) => { + write!(f, "used as a `[timestamp]` attribute") + } Self::WorkingDirectoryAttribute(_) => { write!(f, "used as a `[working-directory]` attribute") } diff --git a/src/unresolved_recipe.rs b/src/unresolved_recipe.rs index 0cf3a19d3f..38afd26ea1 100644 --- a/src/unresolved_recipe.rs +++ b/src/unresolved_recipe.rs @@ -97,7 +97,9 @@ impl<'src> UnresolvedRecipe<'src> { )?; } } - Attribute::Confirm(Some(expression)) | Attribute::WorkingDirectory(expression) => { + Attribute::Confirm(Some(expression)) + | Attribute::Timestamp(Some(expression)) + | Attribute::WorkingDirectory(expression) => { variable_resolver.resolve_expression( expression, ¶meters, @@ -134,7 +136,7 @@ impl<'src> UnresolvedRecipe<'src> { | Attribute::Private | Attribute::Script(_) | Attribute::Shell - | Attribute::Timestamp + | Attribute::Timestamp(None) | Attribute::Unix | Attribute::Windows => {} } diff --git a/tests/timestamps.rs b/tests/timestamps.rs index b2fb5df145..e51c52e9c3 100644 --- a/tests/timestamps.rs +++ b/tests/timestamps.rs @@ -62,6 +62,38 @@ fn attribute() { .success(); } +#[test] +fn attribute_format() { + Test::new() + .justfile( + " + [timestamp('%H:%M:%S%.3f')] + recipe: + echo foo + ", + ) + .stderr_regex(concat!(r"\[\d\d:\d\d:\d\d\.\d\d\d\] echo foo", "\n")) + .stdout("foo\n") + .success(); +} + +#[test] +fn attribute_format_expression() { + Test::new() + .justfile( + " + format := '%H:%M:%S' + '%.3f' + + [timestamp(format)] + recipe: + echo foo + ", + ) + .stderr_regex(concat!(r"\[\d\d:\d\d:\d\d\.\d\d\d\] echo foo", "\n")) + .stdout("foo\n") + .success(); +} + #[test] fn attribute_script() { Test::new()