]> git.proxmox.com Git - cargo.git/blob - src/cargo/core/shell.rs
Auto merge of #7857 - ehuss:fix-build-script-dupe, r=alexcrichton
[cargo.git] / src / cargo / core / shell.rs
1 use std::fmt;
2 use std::io::prelude::*;
3
4 use atty;
5 use termcolor::Color::{Cyan, Green, Red, Yellow};
6 use termcolor::{self, Color, ColorSpec, StandardStream, WriteColor};
7
8 use crate::util::errors::CargoResult;
9
10 /// The requested verbosity of output.
11 #[derive(Debug, Clone, Copy, PartialEq)]
12 pub enum Verbosity {
13 Verbose,
14 Normal,
15 Quiet,
16 }
17
18 /// An abstraction around a `Write`able object that remembers preferences for output verbosity and
19 /// color.
20 pub struct Shell {
21 /// the `Write`able object, either with or without color support (represented by different enum
22 /// variants)
23 err: ShellOut,
24 /// How verbose messages should be
25 verbosity: Verbosity,
26 /// Flag that indicates the current line needs to be cleared before
27 /// printing. Used when a progress bar is currently displayed.
28 needs_clear: bool,
29 }
30
31 impl fmt::Debug for Shell {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 match self.err {
34 ShellOut::Write(_) => f
35 .debug_struct("Shell")
36 .field("verbosity", &self.verbosity)
37 .finish(),
38 ShellOut::Stream { color_choice, .. } => f
39 .debug_struct("Shell")
40 .field("verbosity", &self.verbosity)
41 .field("color_choice", &color_choice)
42 .finish(),
43 }
44 }
45 }
46
47 /// A `Write`able object, either with or without color support
48 enum ShellOut {
49 /// A plain write object without color support
50 Write(Box<dyn Write>),
51 /// Color-enabled stdio, with information on whether color should be used
52 Stream {
53 stream: StandardStream,
54 tty: bool,
55 color_choice: ColorChoice,
56 },
57 }
58
59 /// Whether messages should use color output
60 #[derive(Debug, PartialEq, Clone, Copy)]
61 pub enum ColorChoice {
62 /// Force color output
63 Always,
64 /// Force disable color output
65 Never,
66 /// Intelligently guess whether to use color output
67 CargoAuto,
68 }
69
70 impl Shell {
71 /// Creates a new shell (color choice and verbosity), defaulting to 'auto' color and verbose
72 /// output.
73 pub fn new() -> Shell {
74 Shell {
75 err: ShellOut::Stream {
76 stream: StandardStream::stderr(ColorChoice::CargoAuto.to_termcolor_color_choice()),
77 color_choice: ColorChoice::CargoAuto,
78 tty: atty::is(atty::Stream::Stderr),
79 },
80 verbosity: Verbosity::Verbose,
81 needs_clear: false,
82 }
83 }
84
85 /// Creates a shell from a plain writable object, with no color, and max verbosity.
86 pub fn from_write(out: Box<dyn Write>) -> Shell {
87 Shell {
88 err: ShellOut::Write(out),
89 verbosity: Verbosity::Verbose,
90 needs_clear: false,
91 }
92 }
93
94 /// Prints a message, where the status will have `color` color, and can be justified. The
95 /// messages follows without color.
96 fn print(
97 &mut self,
98 status: &dyn fmt::Display,
99 message: Option<&dyn fmt::Display>,
100 color: Color,
101 justified: bool,
102 ) -> CargoResult<()> {
103 match self.verbosity {
104 Verbosity::Quiet => Ok(()),
105 _ => {
106 if self.needs_clear {
107 self.err_erase_line();
108 }
109 self.err.print(status, message, color, justified)
110 }
111 }
112 }
113
114 pub fn stdout_println(&mut self, message: impl fmt::Display) {
115 if self.needs_clear {
116 self.err_erase_line();
117 }
118 println!("{}", message);
119 }
120
121 /// Sets whether the next print should clear the current line.
122 pub fn set_needs_clear(&mut self, needs_clear: bool) {
123 self.needs_clear = needs_clear;
124 }
125
126 /// Returns `true` if the `needs_clear` flag is unset.
127 pub fn is_cleared(&self) -> bool {
128 !self.needs_clear
129 }
130
131 /// Returns the width of the terminal in spaces, if any.
132 pub fn err_width(&self) -> Option<usize> {
133 match self.err {
134 ShellOut::Stream { tty: true, .. } => imp::stderr_width(),
135 _ => None,
136 }
137 }
138
139 /// Returns `true` if stderr is a tty.
140 pub fn is_err_tty(&self) -> bool {
141 match self.err {
142 ShellOut::Stream { tty, .. } => tty,
143 _ => false,
144 }
145 }
146
147 /// Gets a reference to the underlying writer.
148 pub fn err(&mut self) -> &mut dyn Write {
149 if self.needs_clear {
150 self.err_erase_line();
151 }
152 self.err.as_write()
153 }
154
155 /// Erase from cursor to end of line.
156 pub fn err_erase_line(&mut self) {
157 if let ShellOut::Stream { tty: true, .. } = self.err {
158 imp::err_erase_line(self);
159 self.needs_clear = false;
160 }
161 }
162
163 /// Shortcut to right-align and color green a status message.
164 pub fn status<T, U>(&mut self, status: T, message: U) -> CargoResult<()>
165 where
166 T: fmt::Display,
167 U: fmt::Display,
168 {
169 self.print(&status, Some(&message), Green, true)
170 }
171
172 pub fn status_header<T>(&mut self, status: T) -> CargoResult<()>
173 where
174 T: fmt::Display,
175 {
176 self.print(&status, None, Cyan, true)
177 }
178
179 /// Shortcut to right-align a status message.
180 pub fn status_with_color<T, U>(
181 &mut self,
182 status: T,
183 message: U,
184 color: Color,
185 ) -> CargoResult<()>
186 where
187 T: fmt::Display,
188 U: fmt::Display,
189 {
190 self.print(&status, Some(&message), color, true)
191 }
192
193 /// Runs the callback only if we are in verbose mode.
194 pub fn verbose<F>(&mut self, mut callback: F) -> CargoResult<()>
195 where
196 F: FnMut(&mut Shell) -> CargoResult<()>,
197 {
198 match self.verbosity {
199 Verbosity::Verbose => callback(self),
200 _ => Ok(()),
201 }
202 }
203
204 /// Runs the callback if we are not in verbose mode.
205 pub fn concise<F>(&mut self, mut callback: F) -> CargoResult<()>
206 where
207 F: FnMut(&mut Shell) -> CargoResult<()>,
208 {
209 match self.verbosity {
210 Verbosity::Verbose => Ok(()),
211 _ => callback(self),
212 }
213 }
214
215 /// Prints a red 'error' message.
216 pub fn error<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
217 if self.needs_clear {
218 self.err_erase_line();
219 }
220 self.err.print(&"error", Some(&message), Red, false)
221 }
222
223 /// Prints an amber 'warning' message.
224 pub fn warn<T: fmt::Display>(&mut self, message: T) -> CargoResult<()> {
225 match self.verbosity {
226 Verbosity::Quiet => Ok(()),
227 _ => self.print(&"warning", Some(&message), Yellow, false),
228 }
229 }
230
231 /// Updates the verbosity of the shell.
232 pub fn set_verbosity(&mut self, verbosity: Verbosity) {
233 self.verbosity = verbosity;
234 }
235
236 /// Gets the verbosity of the shell.
237 pub fn verbosity(&self) -> Verbosity {
238 self.verbosity
239 }
240
241 /// Updates the color choice (always, never, or auto) from a string..
242 pub fn set_color_choice(&mut self, color: Option<&str>) -> CargoResult<()> {
243 if let ShellOut::Stream {
244 ref mut stream,
245 ref mut color_choice,
246 ..
247 } = self.err
248 {
249 let cfg = match color {
250 Some("always") => ColorChoice::Always,
251 Some("never") => ColorChoice::Never,
252
253 Some("auto") | None => ColorChoice::CargoAuto,
254
255 Some(arg) => anyhow::bail!(
256 "argument for --color must be auto, always, or \
257 never, but found `{}`",
258 arg
259 ),
260 };
261 *color_choice = cfg;
262 *stream = StandardStream::stderr(cfg.to_termcolor_color_choice());
263 }
264 Ok(())
265 }
266
267 /// Gets the current color choice.
268 ///
269 /// If we are not using a color stream, this will always return `Never`, even if the color
270 /// choice has been set to something else.
271 pub fn color_choice(&self) -> ColorChoice {
272 match self.err {
273 ShellOut::Stream { color_choice, .. } => color_choice,
274 ShellOut::Write(_) => ColorChoice::Never,
275 }
276 }
277
278 /// Whether the shell supports color.
279 pub fn supports_color(&self) -> bool {
280 match &self.err {
281 ShellOut::Write(_) => false,
282 ShellOut::Stream { stream, .. } => stream.supports_color(),
283 }
284 }
285
286 /// Prints a message and translates ANSI escape code into console colors.
287 pub fn print_ansi(&mut self, message: &[u8]) -> CargoResult<()> {
288 if self.needs_clear {
289 self.err_erase_line();
290 }
291 #[cfg(windows)]
292 {
293 if let ShellOut::Stream { stream, .. } = &mut self.err {
294 ::fwdansi::write_ansi(stream, message)?;
295 return Ok(());
296 }
297 }
298 self.err().write_all(message)?;
299 Ok(())
300 }
301 }
302
303 impl Default for Shell {
304 fn default() -> Self {
305 Self::new()
306 }
307 }
308
309 impl ShellOut {
310 /// Prints out a message with a status. The status comes first, and is bold plus the given
311 /// color. The status can be justified, in which case the max width that will right align is
312 /// 12 chars.
313 fn print(
314 &mut self,
315 status: &dyn fmt::Display,
316 message: Option<&dyn fmt::Display>,
317 color: Color,
318 justified: bool,
319 ) -> CargoResult<()> {
320 match *self {
321 ShellOut::Stream { ref mut stream, .. } => {
322 stream.reset()?;
323 stream.set_color(ColorSpec::new().set_bold(true).set_fg(Some(color)))?;
324 if justified {
325 write!(stream, "{:>12}", status)?;
326 } else {
327 write!(stream, "{}", status)?;
328 stream.set_color(ColorSpec::new().set_bold(true))?;
329 write!(stream, ":")?;
330 }
331 stream.reset()?;
332 match message {
333 Some(message) => writeln!(stream, " {}", message)?,
334 None => write!(stream, " ")?,
335 }
336 }
337 ShellOut::Write(ref mut w) => {
338 if justified {
339 write!(w, "{:>12}", status)?;
340 } else {
341 write!(w, "{}:", status)?;
342 }
343 match message {
344 Some(message) => writeln!(w, " {}", message)?,
345 None => write!(w, " ")?,
346 }
347 }
348 }
349 Ok(())
350 }
351
352 /// Gets this object as a `io::Write`.
353 fn as_write(&mut self) -> &mut dyn Write {
354 match *self {
355 ShellOut::Stream { ref mut stream, .. } => stream,
356 ShellOut::Write(ref mut w) => w,
357 }
358 }
359 }
360
361 impl ColorChoice {
362 /// Converts our color choice to termcolor's version.
363 fn to_termcolor_color_choice(self) -> termcolor::ColorChoice {
364 match self {
365 ColorChoice::Always => termcolor::ColorChoice::Always,
366 ColorChoice::Never => termcolor::ColorChoice::Never,
367 ColorChoice::CargoAuto => {
368 if atty::is(atty::Stream::Stderr) {
369 termcolor::ColorChoice::Auto
370 } else {
371 termcolor::ColorChoice::Never
372 }
373 }
374 }
375 }
376 }
377
378 #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))]
379 mod imp {
380 use std::mem;
381
382 use libc;
383
384 use super::Shell;
385
386 pub fn stderr_width() -> Option<usize> {
387 unsafe {
388 let mut winsize: libc::winsize = mem::zeroed();
389 // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
390 // as c_uint but ioctl wants c_ulong.
391 if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
392 return None;
393 }
394 if winsize.ws_col > 0 {
395 Some(winsize.ws_col as usize)
396 } else {
397 None
398 }
399 }
400 }
401
402 pub fn err_erase_line(shell: &mut Shell) {
403 // This is the "EL - Erase in Line" sequence. It clears from the cursor
404 // to the end of line.
405 // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
406 let _ = shell.err.as_write().write_all(b"\x1B[K");
407 }
408 }
409
410 #[cfg(all(
411 unix,
412 not(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))
413 ))]
414 mod imp {
415 pub(super) use super::default_err_erase_line as err_erase_line;
416
417 pub fn stderr_width() -> Option<usize> {
418 None
419 }
420 }
421
422 #[cfg(windows)]
423 mod imp {
424 use std::{cmp, mem, ptr};
425 use winapi::um::fileapi::*;
426 use winapi::um::handleapi::*;
427 use winapi::um::processenv::*;
428 use winapi::um::winbase::*;
429 use winapi::um::wincon::*;
430 use winapi::um::winnt::*;
431
432 pub(super) use super::default_err_erase_line as err_erase_line;
433
434 pub fn stderr_width() -> Option<usize> {
435 unsafe {
436 let stdout = GetStdHandle(STD_ERROR_HANDLE);
437 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
438 if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
439 return Some((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
440 }
441
442 // On mintty/msys/cygwin based terminals, the above fails with
443 // INVALID_HANDLE_VALUE. Use an alternate method which works
444 // in that case as well.
445 let h = CreateFileA(
446 "CONOUT$\0".as_ptr() as *const CHAR,
447 GENERIC_READ | GENERIC_WRITE,
448 FILE_SHARE_READ | FILE_SHARE_WRITE,
449 ptr::null_mut(),
450 OPEN_EXISTING,
451 0,
452 ptr::null_mut(),
453 );
454 if h == INVALID_HANDLE_VALUE {
455 return None;
456 }
457
458 let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
459 let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
460 CloseHandle(h);
461 if rc != 0 {
462 let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
463 // Unfortunately cygwin/mintty does not set the size of the
464 // backing console to match the actual window size. This
465 // always reports a size of 80 or 120 (not sure what
466 // determines that). Use a conservative max of 60 which should
467 // work in most circumstances. ConEmu does some magic to
468 // resize the console correctly, but there's no reasonable way
469 // to detect which kind of terminal we are running in, or if
470 // GetConsoleScreenBufferInfo returns accurate information.
471 return Some(cmp::min(60, width));
472 }
473 None
474 }
475 }
476 }
477
478 #[cfg(any(
479 all(
480 unix,
481 not(any(target_os = "linux", target_os = "macos", target_os = "freebsd"))
482 ),
483 windows,
484 ))]
485 fn default_err_erase_line(shell: &mut Shell) {
486 if let Some(max_width) = imp::stderr_width() {
487 let blank = " ".repeat(max_width);
488 drop(write!(shell.err.as_write(), "{}\r", blank));
489 }
490 }