Skip to content

Commit 6841975

Browse files
joshtriplettMatt Wilkinson
and
Matt Wilkinson
committed
Add IsTerminal trait to determine if a descriptor or handle is a terminal
The UNIX and WASI implementations use `isatty`. The Windows implementation uses the same logic the `atty` crate uses, including the hack needed to detect msys terminals. Implement this trait for `File` and for `Stdin`/`Stdout`/`Stderr` and their locked counterparts on all platforms. On UNIX and WASI, implement it for `BorrowedFd`/`OwnedFd`. On Windows, implement it for `BorrowedHandle`/`OwnedHandle`. Based on rust-lang/rust#91121 Co-authored-by: Matt Wilkinson <mattwilki17@gmail.com>
1 parent c556cf0 commit 6841975

File tree

14 files changed

+152
-35
lines changed

14 files changed

+152
-35
lines changed

std/src/io/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,8 @@ pub(crate) use self::stdio::attempt_print_to_stderr;
266266
#[unstable(feature = "internal_output_capture", issue = "none")]
267267
#[doc(no_inline, hidden)]
268268
pub use self::stdio::set_output_capture;
269+
#[unstable(feature = "is_terminal", issue = "98070")]
270+
pub use self::stdio::IsTerminal;
269271
#[unstable(feature = "print_internals", issue = "none")]
270272
pub use self::stdio::{_eprint, _print};
271273
#[stable(feature = "rust1", since = "1.0.0")]

std/src/io/stdio.rs

+29
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::io::prelude::*;
77

88
use crate::cell::{Cell, RefCell};
99
use crate::fmt;
10+
use crate::fs::File;
1011
use crate::io::{self, BufReader, IoSlice, IoSliceMut, LineWriter, Lines};
1112
use crate::sync::atomic::{AtomicBool, Ordering};
1213
use crate::sync::{Arc, Mutex, MutexGuard, OnceLock};
@@ -1035,6 +1036,34 @@ pub(crate) fn attempt_print_to_stderr(args: fmt::Arguments<'_>) {
10351036
let _ = stderr().write_fmt(args);
10361037
}
10371038

1039+
/// Trait to determine if a descriptor/handle refers to a terminal/tty.
1040+
#[unstable(feature = "is_terminal", issue = "98070")]
1041+
pub trait IsTerminal: crate::sealed::Sealed {
1042+
/// Returns `true` if the descriptor/handle refers to a terminal/tty.
1043+
///
1044+
/// On platforms where Rust does not know how to detect a terminal yet, this will return
1045+
/// `false`. This will also return `false` if an unexpected error occurred, such as from
1046+
/// passing an invalid file descriptor.
1047+
fn is_terminal(&self) -> bool;
1048+
}
1049+
1050+
macro_rules! impl_is_terminal {
1051+
($($t:ty),*$(,)?) => {$(
1052+
#[unstable(feature = "sealed", issue = "none")]
1053+
impl crate::sealed::Sealed for $t {}
1054+
1055+
#[unstable(feature = "is_terminal", issue = "98070")]
1056+
impl IsTerminal for $t {
1057+
#[inline]
1058+
fn is_terminal(&self) -> bool {
1059+
crate::sys::io::is_terminal(self)
1060+
}
1061+
}
1062+
)*}
1063+
}
1064+
1065+
impl_is_terminal!(File, Stdin, StdinLock<'_>, Stdout, StdoutLock<'_>, Stderr, StderrLock<'_>);
1066+
10381067
#[unstable(
10391068
feature = "print_internals",
10401069
reason = "implementation detail which may disappear or be replaced at any time",

std/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@
253253
#![feature(exhaustive_patterns)]
254254
#![feature(if_let_guard)]
255255
#![feature(intra_doc_pointers)]
256+
#![feature(is_terminal)]
256257
#![feature(lang_items)]
257258
#![feature(let_chains)]
258259
#![feature(linkage)]

std/src/os/fd/owned.rs

+17
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,23 @@ impl fmt::Debug for OwnedFd {
193193
}
194194
}
195195

196+
macro_rules! impl_is_terminal {
197+
($($t:ty),*$(,)?) => {$(
198+
#[unstable(feature = "sealed", issue = "none")]
199+
impl crate::sealed::Sealed for $t {}
200+
201+
#[unstable(feature = "is_terminal", issue = "98070")]
202+
impl crate::io::IsTerminal for $t {
203+
#[inline]
204+
fn is_terminal(&self) -> bool {
205+
crate::sys::io::is_terminal(self)
206+
}
207+
}
208+
)*}
209+
}
210+
211+
impl_is_terminal!(BorrowedFd<'_>, OwnedFd);
212+
196213
/// A trait to borrow the file descriptor from an underlying object.
197214
///
198215
/// This is only available on unix platforms and must be imported in order to

std/src/os/windows/io/handle.rs

+17
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,23 @@ impl fmt::Debug for OwnedHandle {
384384
}
385385
}
386386

387+
macro_rules! impl_is_terminal {
388+
($($t:ty),*$(,)?) => {$(
389+
#[unstable(feature = "sealed", issue = "none")]
390+
impl crate::sealed::Sealed for $t {}
391+
392+
#[unstable(feature = "is_terminal", issue = "98070")]
393+
impl crate::io::IsTerminal for $t {
394+
#[inline]
395+
fn is_terminal(&self) -> bool {
396+
crate::sys::io::is_terminal(self)
397+
}
398+
}
399+
)*}
400+
}
401+
402+
impl_is_terminal!(BorrowedHandle<'_>, OwnedHandle);
403+
387404
/// A trait to borrow the handle from an underlying object.
388405
#[stable(feature = "io_safety", since = "1.63.0")]
389406
pub trait AsHandle {

std/src/sys/unix/io.rs

+6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::marker::PhantomData;
2+
use crate::os::fd::{AsFd, AsRawFd};
23
use crate::slice;
34

45
use libc::{c_void, iovec};
@@ -74,3 +75,8 @@ impl<'a> IoSliceMut<'a> {
7475
unsafe { slice::from_raw_parts_mut(self.vec.iov_base as *mut u8, self.vec.iov_len) }
7576
}
7677
}
78+
79+
pub fn is_terminal(fd: &impl AsFd) -> bool {
80+
let fd = fd.as_fd();
81+
unsafe { libc::isatty(fd.as_raw_fd()) != 0 }
82+
}

std/src/sys/unsupported/io.rs

+4
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,7 @@ impl<'a> IoSliceMut<'a> {
4545
self.0
4646
}
4747
}
48+
49+
pub fn is_terminal<T>(_: &T) -> bool {
50+
false
51+
}

std/src/sys/wasi/io.rs

+6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#![deny(unsafe_op_in_unsafe_fn)]
22

33
use crate::marker::PhantomData;
4+
use crate::os::fd::{AsFd, AsRawFd};
45
use crate::slice;
56

67
#[derive(Copy, Clone)]
@@ -71,3 +72,8 @@ impl<'a> IoSliceMut<'a> {
7172
unsafe { slice::from_raw_parts_mut(self.vec.buf as *mut u8, self.vec.buf_len) }
7273
}
7374
}
75+
76+
pub fn is_terminal(fd: &impl AsFd) -> bool {
77+
let fd = fd.as_fd();
78+
unsafe { libc::isatty(fd.as_raw_fd()) != 0 }
79+
}

std/src/sys/windows/c.rs

+8
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ pub const SECURITY_SQOS_PRESENT: DWORD = 0x00100000;
127127

128128
pub const FIONBIO: c_ulong = 0x8004667e;
129129

130+
pub const MAX_PATH: usize = 260;
131+
130132
#[repr(C)]
131133
#[derive(Copy)]
132134
pub struct WIN32_FIND_DATAW {
@@ -538,6 +540,12 @@ pub struct SYMBOLIC_LINK_REPARSE_BUFFER {
538540

539541
/// NB: Use carefully! In general using this as a reference is likely to get the
540542
/// provenance wrong for the `PathBuffer` field!
543+
#[repr(C)]
544+
pub struct FILE_NAME_INFO {
545+
pub FileNameLength: DWORD,
546+
pub FileName: [WCHAR; 1],
547+
}
548+
541549
#[repr(C)]
542550
pub struct MOUNT_POINT_REPARSE_BUFFER {
543551
pub SubstituteNameOffset: c_ushort,

std/src/sys/windows/io.rs

+59
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
use crate::marker::PhantomData;
2+
use crate::mem::size_of;
3+
use crate::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
24
use crate::slice;
35
use crate::sys::c;
6+
use core;
7+
use libc;
48

59
#[derive(Copy, Clone)]
610
#[repr(transparent)]
@@ -78,3 +82,58 @@ impl<'a> IoSliceMut<'a> {
7882
unsafe { slice::from_raw_parts_mut(self.vec.buf as *mut u8, self.vec.len as usize) }
7983
}
8084
}
85+
86+
pub fn is_terminal(h: &impl AsHandle) -> bool {
87+
unsafe { handle_is_console(h.as_handle()) }
88+
}
89+
90+
unsafe fn handle_is_console(handle: BorrowedHandle<'_>) -> bool {
91+
let handle = handle.as_raw_handle();
92+
93+
let mut out = 0;
94+
if c::GetConsoleMode(handle, &mut out) != 0 {
95+
// False positives aren't possible. If we got a console then we definitely have a console.
96+
return true;
97+
}
98+
99+
// At this point, we *could* have a false negative. We can determine that this is a true
100+
// negative if we can detect the presence of a console on any of the standard I/O streams. If
101+
// another stream has a console, then we know we're in a Windows console and can therefore
102+
// trust the negative.
103+
for std_handle in [c::STD_INPUT_HANDLE, c::STD_OUTPUT_HANDLE, c::STD_ERROR_HANDLE] {
104+
let std_handle = c::GetStdHandle(std_handle);
105+
if std_handle != handle && c::GetConsoleMode(std_handle, &mut out) != 0 {
106+
return false;
107+
}
108+
}
109+
110+
// Otherwise, we fall back to an msys hack to see if we can detect the presence of a pty.
111+
msys_tty_on(handle)
112+
}
113+
114+
unsafe fn msys_tty_on(handle: c::HANDLE) -> bool {
115+
let size = size_of::<c::FILE_NAME_INFO>() + c::MAX_PATH * size_of::<c::WCHAR>();
116+
let mut name_info_bytes = vec![0u8; size];
117+
let res = c::GetFileInformationByHandleEx(
118+
handle,
119+
c::FileNameInfo,
120+
name_info_bytes.as_mut_ptr() as *mut libc::c_void,
121+
size as u32,
122+
);
123+
if res == 0 {
124+
return false;
125+
}
126+
let name_info: &c::FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const c::FILE_NAME_INFO);
127+
let s = core::slice::from_raw_parts(
128+
name_info.FileName.as_ptr(),
129+
name_info.FileNameLength as usize / 2,
130+
);
131+
let name = String::from_utf16_lossy(s);
132+
// This checks whether 'pty' exists in the file name, which indicates that
133+
// a pseudo-terminal is attached. To mitigate against false positives
134+
// (e.g., an actual file name that contains 'pty'), we also require that
135+
// either the strings 'msys-' or 'cygwin-' are in the file name as well.)
136+
let is_msys = name.contains("msys-") || name.contains("cygwin-");
137+
let is_pty = name.contains("-pty");
138+
is_msys && is_pty
139+
}

test/src/cli.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
use std::env;
44
use std::path::PathBuf;
55

6-
use super::helpers::isatty;
76
use super::options::{ColorConfig, Options, OutputFormat, RunIgnored};
87
use super::time::TestTimeOptions;
8+
use std::io::{self, IsTerminal};
99

1010
#[derive(Debug)]
1111
pub struct TestOpts {
@@ -32,7 +32,7 @@ pub struct TestOpts {
3232
impl TestOpts {
3333
pub fn use_color(&self) -> bool {
3434
match self.color {
35-
ColorConfig::AutoColor => !self.nocapture && isatty::stdout_isatty(),
35+
ColorConfig::AutoColor => !self.nocapture && io::stdout().is_terminal(),
3636
ColorConfig::AlwaysColor => true,
3737
ColorConfig::NeverColor => false,
3838
}

test/src/helpers/isatty.rs

-32
This file was deleted.

test/src/helpers/mod.rs

-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,5 @@
33
44
pub mod concurrency;
55
pub mod exit_code;
6-
pub mod isatty;
76
pub mod metrics;
87
pub mod shuffle;

test/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#![unstable(feature = "test", issue = "50297")]
1818
#![doc(test(attr(deny(warnings))))]
1919
#![feature(internal_output_capture)]
20+
#![feature(is_terminal)]
2021
#![feature(staged_api)]
2122
#![feature(process_exitcode_internals)]
2223
#![feature(test)]

0 commit comments

Comments
 (0)