@@ -2,6 +2,7 @@ use std::env;
22use std:: io:: { self , BufRead , BufReader , Write , BufWriter } ;
33use std:: path:: Path ;
44use std:: time:: { SystemTime , UNIX_EPOCH , Instant } ;
5+ use std:: fs:: { File , create_dir_all} ;
56use chrono:: { DateTime , Local , Utc , TimeZone , Timelike , Datelike } ;
67
78struct Config {
@@ -19,6 +20,7 @@ struct Config {
1920 color : bool ,
2021 buffered : bool ,
2122 timezone : Option < String > ,
23+ output_file : Option < String > ,
2224}
2325
2426impl Config {
@@ -38,6 +40,7 @@ impl Config {
3840 color : false ,
3941 buffered : false , // Default to unbuffered for real-time output
4042 timezone : None ,
43+ output_file : None ,
4144 } ;
4245
4346 let args: Vec < String > = env:: args ( ) . collect ( ) ;
@@ -88,6 +91,14 @@ impl Config {
8891 }
8992 config. timezone = Some ( args[ i] . clone ( ) ) ;
9093 }
94+ "-o" | "--output" => {
95+ i += 1 ;
96+ if i >= args. len ( ) {
97+ eprintln ! ( "Error: --output requires a value" ) ;
98+ std:: process:: exit ( 1 ) ;
99+ }
100+ config. output_file = Some ( args[ i] . clone ( ) ) ;
101+ }
91102 _ => {
92103 eprintln ! ( "Unknown argument: {}" , args[ i] ) ;
93104 std:: process:: exit ( 1 ) ;
@@ -142,6 +153,7 @@ Options:
142153 --color Colorize timestamps
143154 --buffered Use buffered output (default is unbuffered)
144155 --timezone TZ Use specific timezone (e.g., UTC, EST, PST)
156+ -o, --output FILE Write timestamped output to file (creates directories)
145157 -h, --help Show this help
146158
147159Format specifiers (strftime compatible):
@@ -160,10 +172,13 @@ Examples:
160172 cat file.txt | {} --delta # Show time between lines
161173 ping host | {} --color --microseconds # Colored with microseconds
162174 command | {} --prefix-only # Only timestamps
175+ make 2>&1 | {} -o build.log # Stream to console and file
176+ tail -f app.log | {} -o logs/app-timestamped.log # Save to file with directories
163177
164178Note: --relative and --delta are mutually exclusive" ,
165179 program_name, program_name, program_name, program_name, program_name,
166- program_name, program_name, program_name, program_name, program_name, program_name
180+ program_name, program_name, program_name, program_name, program_name,
181+ program_name, program_name, program_name
167182 ) ;
168183 }
169184}
@@ -272,6 +287,16 @@ impl TimeFormatter {
272287
273288 #[ inline]
274289 fn format_timestamp ( & mut self , monotonic : bool ) -> & str {
290+ self . format_timestamp_impl ( monotonic, true )
291+ }
292+
293+ #[ inline]
294+ fn format_timestamp_no_color ( & mut self , monotonic : bool ) -> & str {
295+ self . format_timestamp_impl ( monotonic, false )
296+ }
297+
298+ #[ inline]
299+ fn format_timestamp_impl ( & mut self , monotonic : bool , use_color : bool ) -> & str {
275300 self . timestamp_buf . clear ( ) ;
276301
277302 match & self . format_type {
@@ -283,7 +308,7 @@ impl TimeFormatter {
283308 } else {
284309 // Initialize with current time for first call
285310 self . last_instant = Some ( instant) ;
286- return if self . color {
311+ return if use_color && self . color {
287312 self . timestamp_buf . push_str ( self . color_prefix ) ;
288313 self . timestamp_buf . push_str ( "0.000000" ) ;
289314 self . timestamp_buf . push_str ( self . color_suffix ) ;
@@ -301,7 +326,7 @@ impl TimeFormatter {
301326 } else {
302327 // Initialize with current time for first call
303328 self . last_time = Some ( time) ;
304- return if self . color {
329+ return if use_color && self . color {
305330 self . timestamp_buf . push_str ( self . color_prefix ) ;
306331 self . timestamp_buf . push_str ( "0.000000" ) ;
307332 self . timestamp_buf . push_str ( self . color_suffix ) ;
@@ -480,7 +505,7 @@ impl TimeFormatter {
480505
481506 // Add color codes if needed - do this outside the timestamp buffer
482507 // to avoid reallocation on every call
483- if self . color {
508+ if use_color && self . color {
484509 // Create a temporary string with color codes
485510 let colored = format ! ( "{}{}{}" , self . color_prefix, self . timestamp_buf, self . color_suffix) ;
486511 self . timestamp_buf = colored;
@@ -497,6 +522,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
497522 let stdin = io:: stdin ( ) ;
498523 let stdout = io:: stdout ( ) ;
499524
525+ // Set up output file if specified
526+ let mut file_writer = if let Some ( ref output_path) = config. output_file {
527+ // Create parent directories if they don't exist
528+ if let Some ( parent) = Path :: new ( output_path) . parent ( ) {
529+ create_dir_all ( parent) ?;
530+ }
531+ Some ( BufWriter :: new ( File :: create ( output_path) ?) )
532+ } else {
533+ None
534+ } ;
535+
500536 // Use appropriate buffer sizes based on configuration
501537 let buffer_size = if config. buffered { 256 * 1024 } else { 0 } ;
502538 let reader = BufReader :: with_capacity ( 128 * 1024 , stdin) ;
@@ -509,22 +545,54 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
509545 let line = line_result?;
510546 let timestamp = formatter. format_timestamp ( config. monotonic ) ;
511547
512- // Write timestamp
513- writer. write_all ( timestamp. as_bytes ( ) ) ?;
548+ // Prepare the complete output line
549+ let mut output_line = Vec :: new ( ) ;
550+ output_line. extend_from_slice ( timestamp. as_bytes ( ) ) ;
514551
515552 if !config. prefix_only {
516- writer . write_all ( separator_bytes) ? ;
517- writer . write_all ( line. as_bytes ( ) ) ? ;
553+ output_line . extend_from_slice ( separator_bytes) ;
554+ output_line . extend_from_slice ( line. as_bytes ( ) ) ;
518555 }
519556
520- writer. write_all ( newline) ?;
557+ output_line. extend_from_slice ( newline) ;
558+
559+ // Write to stdout
560+ writer. write_all ( & output_line) ?;
561+
562+ // Write to file if specified (without color codes for clean file output)
563+ if let Some ( ref mut file_writer) = file_writer {
564+ if config. color {
565+ // Strip color codes for file output
566+ let clean_timestamp = formatter. format_timestamp_no_color ( config. monotonic ) ;
567+ let mut clean_output = Vec :: new ( ) ;
568+ clean_output. extend_from_slice ( clean_timestamp. as_bytes ( ) ) ;
569+
570+ if !config. prefix_only {
571+ clean_output. extend_from_slice ( separator_bytes) ;
572+ clean_output. extend_from_slice ( line. as_bytes ( ) ) ;
573+ }
574+
575+ clean_output. extend_from_slice ( newline) ;
576+ file_writer. write_all ( & clean_output) ?;
577+ } else {
578+ file_writer. write_all ( & output_line) ?;
579+ }
580+
581+ // Flush file writer if not buffered
582+ if !config. buffered {
583+ file_writer. flush ( ) ?;
584+ }
585+ }
521586
522- // Flush when unbuffered
587+ // Flush stdout when unbuffered
523588 if !config. buffered {
524589 writer. flush ( ) ?;
525590 }
526591 }
527592
528593 writer. flush ( ) ?;
594+ if let Some ( ref mut file_writer) = file_writer {
595+ file_writer. flush ( ) ?;
596+ }
529597 Ok ( ( ) )
530598}
0 commit comments