├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── screenshot.png └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" 10 | 11 | [[package]] 12 | name = "anstyle-git" 13 | version = "1.0.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "6c49672748e330c651b3eafff80b14574a094192dd2864e4b1c8661358ce2df9" 16 | dependencies = [ 17 | "anstyle", 18 | ] 19 | 20 | [[package]] 21 | name = "anyhow" 22 | version = "1.0.72" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.1.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "1.3.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 37 | 38 | [[package]] 39 | name = "bitflags" 40 | version = "2.3.3" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" 43 | 44 | [[package]] 45 | name = "cc" 46 | version = "1.0.79" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 49 | 50 | [[package]] 51 | name = "cfg-if" 52 | version = "1.0.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 55 | 56 | [[package]] 57 | name = "errno" 58 | version = "0.3.1" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" 61 | dependencies = [ 62 | "errno-dragonfly", 63 | "libc", 64 | "windows-sys", 65 | ] 66 | 67 | [[package]] 68 | name = "errno-dragonfly" 69 | version = "0.1.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 72 | dependencies = [ 73 | "cc", 74 | "libc", 75 | ] 76 | 77 | [[package]] 78 | name = "fastrand" 79 | version = "1.9.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 82 | dependencies = [ 83 | "instant", 84 | ] 85 | 86 | [[package]] 87 | name = "fastrand" 88 | version = "2.0.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" 91 | 92 | [[package]] 93 | name = "hermit-abi" 94 | version = "0.3.2" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" 97 | 98 | [[package]] 99 | name = "highlight-stderr" 100 | version = "0.2.1" 101 | dependencies = [ 102 | "anstyle-git", 103 | "anyhow", 104 | "io-mux", 105 | ] 106 | 107 | [[package]] 108 | name = "instant" 109 | version = "0.1.12" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 112 | dependencies = [ 113 | "cfg-if", 114 | ] 115 | 116 | [[package]] 117 | name = "io-lifetimes" 118 | version = "1.0.11" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 121 | dependencies = [ 122 | "hermit-abi", 123 | "libc", 124 | "windows-sys", 125 | ] 126 | 127 | [[package]] 128 | name = "io-mux" 129 | version = "2.1.0" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "5b722f0afeea4a6a59f2203af40893179c827c5026d2189624aed747060c43f2" 132 | dependencies = [ 133 | "fastrand 2.0.0", 134 | "rustix 0.38.4", 135 | "tempfile", 136 | ] 137 | 138 | [[package]] 139 | name = "libc" 140 | version = "0.2.147" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" 143 | 144 | [[package]] 145 | name = "linux-raw-sys" 146 | version = "0.3.8" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 149 | 150 | [[package]] 151 | name = "linux-raw-sys" 152 | version = "0.4.3" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" 155 | 156 | [[package]] 157 | name = "redox_syscall" 158 | version = "0.3.5" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" 161 | dependencies = [ 162 | "bitflags 1.3.2", 163 | ] 164 | 165 | [[package]] 166 | name = "rustix" 167 | version = "0.37.23" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" 170 | dependencies = [ 171 | "bitflags 1.3.2", 172 | "errno", 173 | "io-lifetimes", 174 | "libc", 175 | "linux-raw-sys 0.3.8", 176 | "windows-sys", 177 | ] 178 | 179 | [[package]] 180 | name = "rustix" 181 | version = "0.38.4" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" 184 | dependencies = [ 185 | "bitflags 2.3.3", 186 | "errno", 187 | "libc", 188 | "linux-raw-sys 0.4.3", 189 | "windows-sys", 190 | ] 191 | 192 | [[package]] 193 | name = "tempfile" 194 | version = "3.6.0" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" 197 | dependencies = [ 198 | "autocfg", 199 | "cfg-if", 200 | "fastrand 1.9.0", 201 | "redox_syscall", 202 | "rustix 0.37.23", 203 | "windows-sys", 204 | ] 205 | 206 | [[package]] 207 | name = "windows-sys" 208 | version = "0.48.0" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 211 | dependencies = [ 212 | "windows-targets", 213 | ] 214 | 215 | [[package]] 216 | name = "windows-targets" 217 | version = "0.48.1" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" 220 | dependencies = [ 221 | "windows_aarch64_gnullvm", 222 | "windows_aarch64_msvc", 223 | "windows_i686_gnu", 224 | "windows_i686_msvc", 225 | "windows_x86_64_gnu", 226 | "windows_x86_64_gnullvm", 227 | "windows_x86_64_msvc", 228 | ] 229 | 230 | [[package]] 231 | name = "windows_aarch64_gnullvm" 232 | version = "0.48.0" 233 | source = "registry+https://github.com/rust-lang/crates.io-index" 234 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 235 | 236 | [[package]] 237 | name = "windows_aarch64_msvc" 238 | version = "0.48.0" 239 | source = "registry+https://github.com/rust-lang/crates.io-index" 240 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 241 | 242 | [[package]] 243 | name = "windows_i686_gnu" 244 | version = "0.48.0" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 247 | 248 | [[package]] 249 | name = "windows_i686_msvc" 250 | version = "0.48.0" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 253 | 254 | [[package]] 255 | name = "windows_x86_64_gnu" 256 | version = "0.48.0" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 259 | 260 | [[package]] 261 | name = "windows_x86_64_gnullvm" 262 | version = "0.48.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 265 | 266 | [[package]] 267 | name = "windows_x86_64_msvc" 268 | version = "0.48.0" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 271 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "highlight-stderr" 3 | version = "0.3.0" 4 | authors = ["Josh Triplett "] 5 | description = "Run a command and highlight its stderr, preserving the order of stdout and stderr" 6 | keywords = ["log", "pipe", "stdout", "stderr"] 7 | categories = ["command-line-utilities"] 8 | repository = "https://github.com/joshtriplett/highlight-stderr" 9 | edition = "2021" 10 | license = "MIT OR Apache-2.0" 11 | 12 | [dependencies] 13 | anstyle-git = "1" 14 | anyhow = "1.0.72" 15 | io-mux = "2.1.0" 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `highlight-stderr` 2 | 3 | `highlight-stderr` runs a command and highlights the stderr of that command, 4 | while preserving the order of stdout and stderr. It sends both stdout and 5 | stderr of the command to its stdout, making it easy to pipe to a pager or 6 | similar. 7 | 8 | ![Screenshot of highlight-stderr](screenshot.png) 9 | 10 | By default, `highlight-stderr` will highlight stderr in bold red, and leave 11 | stdout normal. However, you can configure `highlight-stderr` using the 12 | environment variables `HIGHLIGHT_STDERR` and `HIGHLIGHT_STDOUT`, which can each 13 | contain a color specification in git's color specification format, such as 14 | `bold white red`. 15 | 16 | `highlight-stderr` uses [`io-mux`](https://crates.io/crates/io-mux) to capture 17 | the stdout and stderr of the command. 18 | 19 | `highlight-stderr` only runs on Linux. 20 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshtriplett/highlight-stderr/0d4ebdefd599149ecba77bcd3f1f27bc297aa62b/screenshot.png -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | use std::os::unix::process::ExitStatusExt; 3 | use std::process::Command; 4 | 5 | use anyhow::bail; 6 | use io_mux::{Mux, TaggedData}; 7 | 8 | fn env_or(varname: &str, default: &str) -> String { 9 | std::env::var_os(varname) 10 | .map(|v| v.to_string_lossy().into_owned()) 11 | .unwrap_or_else(|| default.to_string()) 12 | } 13 | 14 | fn main() -> anyhow::Result<()> { 15 | let mut mux = Mux::new()?; 16 | 17 | let mut args = std::env::args_os().skip(1); 18 | let cmd = if let Some(cmd) = args.next() { 19 | cmd 20 | } else { 21 | bail!("Usage: highlight-stderr command [args]"); 22 | }; 23 | let (out_tag, out_sender) = mux.make_sender()?; 24 | let (err_tag, err_sender) = mux.make_sender()?; 25 | let mut child = Command::new(&cmd) 26 | .args(args) 27 | .stdout(out_sender) 28 | .stderr(err_sender) 29 | .spawn()?; 30 | 31 | let (done_tag, mut done_sender) = mux.make_sender()?; 32 | std::thread::spawn(move || match child.wait() { 33 | Ok(status) => { 34 | let exit_code = if let Some(code) = status.code() { 35 | code as u8 36 | } else { 37 | status.signal().unwrap() as u8 + 128 38 | }; 39 | let _ = done_sender.write_all(&[exit_code]); 40 | } 41 | Err(e) => { 42 | let _ = write!(done_sender, "Error: {:?}\n", e); 43 | } 44 | }); 45 | 46 | let highlight_stdout = anstyle_git::parse(&env_or("HIGHLIGHT_STDOUT", ""))?; 47 | let highlight_stderr = anstyle_git::parse(&env_or("HIGHLIGHT_STDERR", "bold red"))?; 48 | let out_raw = std::io::stdout(); 49 | let out = &mut out_raw.lock(); 50 | 51 | loop { 52 | let TaggedData { tag, data } = mux.read()?; 53 | if tag == out_tag { 54 | highlight_stdout.write_to(out)?; 55 | out.write_all(data)?; 56 | highlight_stdout.write_reset_to(out)?; 57 | } else if tag == err_tag { 58 | highlight_stderr.write_to(out)?; 59 | out.write_all(data)?; 60 | highlight_stderr.write_reset_to(out)?; 61 | } else if tag == done_tag { 62 | match data { 63 | &[exit_code] => std::process::exit(exit_code as i32), 64 | error => { 65 | std::io::stderr().write_all(error)?; 66 | std::process::exit(1); 67 | } 68 | } 69 | } else { 70 | bail!("Unexpected tag {tag:?}"); 71 | } 72 | } 73 | } 74 | --------------------------------------------------------------------------------