|
| 1 | +use std::env; |
| 2 | +use std::io::{self, BufRead, BufReader, Write}; |
| 3 | +use std::time::{SystemTime, UNIX_EPOCH, Instant}; |
| 4 | + |
| 5 | +struct Config { |
| 6 | + format: String, |
| 7 | + separator: String, |
| 8 | + relative: bool, |
| 9 | + monotonic: bool, |
| 10 | + utc: bool, |
| 11 | +} |
| 12 | + |
| 13 | +impl Config { |
| 14 | + fn parse_args() -> Result<Self, Box<dyn std::error::Error>> { |
| 15 | + let mut config = Config { |
| 16 | + format: "%Y-%m-%d %H:%M:%S".to_string(), |
| 17 | + separator: " ".to_string(), |
| 18 | + relative: false, |
| 19 | + monotonic: false, |
| 20 | + utc: false, |
| 21 | + }; |
| 22 | + |
| 23 | + let args: Vec<String> = env::args().collect(); |
| 24 | + let mut i = 1; |
| 25 | + |
| 26 | + while i < args.len() { |
| 27 | + match args[i].as_str() { |
| 28 | + "-h" | "--help" => { |
| 29 | + Self::print_help(); |
| 30 | + std::process::exit(0); |
| 31 | + } |
| 32 | + "-r" | "--relative" => config.relative = true, |
| 33 | + "-m" | "--monotonic" => config.monotonic = true, |
| 34 | + "-u" | "--utc" => config.utc = true, |
| 35 | + "-s" | "--separator" => { |
| 36 | + i += 1; |
| 37 | + if i >= args.len() { |
| 38 | + eprintln!("Error: --separator requires a value"); |
| 39 | + std::process::exit(1); |
| 40 | + } |
| 41 | + config.separator = args[i].clone(); |
| 42 | + } |
| 43 | + "-f" | "--format" => { |
| 44 | + i += 1; |
| 45 | + if i >= args.len() { |
| 46 | + eprintln!("Error: --format requires a value"); |
| 47 | + std::process::exit(1); |
| 48 | + } |
| 49 | + config.format = args[i].clone(); |
| 50 | + } |
| 51 | + _ => { |
| 52 | + eprintln!("Unknown argument: {}", args[i]); |
| 53 | + std::process::exit(1); |
| 54 | + } |
| 55 | + } |
| 56 | + i += 1; |
| 57 | + } |
| 58 | + |
| 59 | + Ok(config) |
| 60 | + } |
| 61 | + |
| 62 | + fn print_help() { |
| 63 | + println!( |
| 64 | + "ts - timestamp each line of input |
| 65 | +
|
| 66 | +Usage: ts [OPTIONS] |
| 67 | +
|
| 68 | +Options: |
| 69 | + -f, --format FORMAT Date format (default: %Y-%m-%d %H:%M:%S) |
| 70 | + -s, --separator SEP Separator between timestamp and line (default: \" \") |
| 71 | + -r, --relative Show relative timestamps from start |
| 72 | + -m, --monotonic Use monotonic clock for relative timestamps |
| 73 | + -u, --utc Use UTC time |
| 74 | + -h, --help Show this help |
| 75 | +
|
| 76 | +Examples: |
| 77 | + ls -la | ts |
| 78 | + tail -f /var/log/messages | ts -r |
| 79 | + ping google.com | ts -f \"%H:%M:%S.%f\" |
| 80 | +" |
| 81 | + ); |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +struct TimeFormatter { |
| 86 | + format: String, |
| 87 | + relative: bool, |
| 88 | + utc: bool, |
| 89 | + start_time: Option<SystemTime>, |
| 90 | + start_instant: Option<Instant>, |
| 91 | +} |
| 92 | + |
| 93 | +impl TimeFormatter { |
| 94 | + fn new(config: &Config) -> Self { |
| 95 | + Self { |
| 96 | + format: config.format.clone(), |
| 97 | + relative: config.relative, |
| 98 | + utc: config.utc, |
| 99 | + start_time: None, |
| 100 | + start_instant: None, |
| 101 | + } |
| 102 | + } |
| 103 | + |
| 104 | + fn format_timestamp(&mut self, monotonic: bool) -> String { |
| 105 | + if self.relative { |
| 106 | + if monotonic { |
| 107 | + let now = Instant::now(); |
| 108 | + let start = self.start_instant.get_or_insert(now); |
| 109 | + let duration = now.duration_since(*start); |
| 110 | + format!("{}.{:03}", duration.as_secs(), duration.subsec_millis()) |
| 111 | + } else { |
| 112 | + let now = SystemTime::now(); |
| 113 | + let start = self.start_time.get_or_insert(now); |
| 114 | + let duration = now.duration_since(*start).unwrap_or_default(); |
| 115 | + format!("{}.{:03}", duration.as_secs(), duration.subsec_millis()) |
| 116 | + } |
| 117 | + } else { |
| 118 | + let now = SystemTime::now(); |
| 119 | + let duration = now.duration_since(UNIX_EPOCH).unwrap(); |
| 120 | + let secs = duration.as_secs(); |
| 121 | + let nanos = duration.subsec_nanos(); |
| 122 | + let micros = nanos / 1000; |
| 123 | + |
| 124 | + // Fast path for common format |
| 125 | + if self.format == "%Y-%m-%d %H:%M:%S" { |
| 126 | + let dt = secs as i64; |
| 127 | + let (year, month, day, hour, min, sec) = Self::timestamp_to_parts(dt, self.utc); |
| 128 | + return format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", |
| 129 | + year, month, day, hour, min, sec); |
| 130 | + } |
| 131 | + |
| 132 | + // Handle custom formats |
| 133 | + self.format_custom_timestamp(secs, micros) |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + #[inline] |
| 138 | + fn timestamp_to_parts(timestamp: i64, utc: bool) -> (i32, u32, u32, u32, u32, u32) { |
| 139 | + // Simple UTC conversion (leap seconds ignored for performance) |
| 140 | + let mut days = timestamp / 86400; |
| 141 | + let remaining_secs = (timestamp % 86400) as u32; |
| 142 | + |
| 143 | + if !utc { |
| 144 | + // Rough local timezone offset - in practice you'd want proper timezone handling |
| 145 | + // This is simplified for maximum performance |
| 146 | + } |
| 147 | + |
| 148 | + let hour = remaining_secs / 3600; |
| 149 | + let min = (remaining_secs % 3600) / 60; |
| 150 | + let sec = remaining_secs % 60; |
| 151 | + |
| 152 | + // Calculate date from days since epoch (1970-01-01) |
| 153 | + days += 719468; // Days from year 1 to 1970 |
| 154 | + |
| 155 | + let era = days / 146097; |
| 156 | + let doe = days % 146097; |
| 157 | + let yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365; |
| 158 | + let year = yoe + era * 400; |
| 159 | + let doy = doe - (365*yoe + yoe/4 - yoe/100); |
| 160 | + let mp = (5*doy + 2)/153; |
| 161 | + let day = doy - (153*mp+2)/5 + 1; |
| 162 | + let month = mp + if mp < 10 { 3 } else { -9 }; |
| 163 | + let year = year + if month <= 2 { 1 } else { 0 }; |
| 164 | + |
| 165 | + (year as i32, month as u32, day as u32, hour, min, sec) |
| 166 | + } |
| 167 | + |
| 168 | + fn format_custom_timestamp(&self, secs: u64, micros: u32) -> String { |
| 169 | + let (year, month, day, hour, min, sec) = Self::timestamp_to_parts(secs as i64, self.utc); |
| 170 | + |
| 171 | + // Simple format replacement for performance |
| 172 | + let mut result = self.format.clone(); |
| 173 | + result = result.replace("%Y", &format!("{:04}", year)); |
| 174 | + result = result.replace("%m", &format!("{:02}", month)); |
| 175 | + result = result.replace("%d", &format!("{:02}", day)); |
| 176 | + result = result.replace("%H", &format!("{:02}", hour)); |
| 177 | + result = result.replace("%M", &format!("{:02}", min)); |
| 178 | + result = result.replace("%S", &format!("{:02}", sec)); |
| 179 | + result = result.replace("%f", &format!("{:06}", micros)); |
| 180 | + result = result.replace("%%", "%"); |
| 181 | + |
| 182 | + result |
| 183 | + } |
| 184 | +} |
| 185 | + |
| 186 | +fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 187 | + let config = Config::parse_args()?; |
| 188 | + let mut formatter = TimeFormatter::new(&config); |
| 189 | + |
| 190 | + let stdin = io::stdin(); |
| 191 | + let mut stdout = io::stdout(); |
| 192 | + |
| 193 | + // Use buffered reader for better performance |
| 194 | + let reader = BufReader::with_capacity(65536, stdin); |
| 195 | + |
| 196 | + // Pre-allocate output buffer |
| 197 | + let mut output_buf = String::with_capacity(1024); |
| 198 | + |
| 199 | + for line in reader.lines() { |
| 200 | + let line = line?; |
| 201 | + let timestamp = formatter.format_timestamp(config.monotonic); |
| 202 | + |
| 203 | + // Build output in buffer to minimize syscalls |
| 204 | + output_buf.clear(); |
| 205 | + output_buf.push_str(×tamp); |
| 206 | + output_buf.push_str(&config.separator); |
| 207 | + output_buf.push_str(&line); |
| 208 | + output_buf.push('\n'); |
| 209 | + |
| 210 | + stdout.write_all(output_buf.as_bytes())?; |
| 211 | + } |
| 212 | + |
| 213 | + Ok(()) |
| 214 | +} |
0 commit comments