mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
stty: use stdin for TTY operations instead of /dev/tty (#9881)
* stty: use stdin for TTY operations instead of /dev/tty * Add tests for stty stdin behavior and fix platform-specific error messages --------- Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
This commit is contained in:
@@ -379,6 +379,7 @@ istrip
|
||||
litout
|
||||
opost
|
||||
parodd
|
||||
ENOTTY
|
||||
|
||||
# translation tests
|
||||
CLICOLOR
|
||||
|
||||
+34
-37
@@ -26,12 +26,12 @@ use nix::sys::termios::{
|
||||
use nix::{ioctl_read_bad, ioctl_write_ptr_bad};
|
||||
use std::cmp::Ordering;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Stdout, stdout};
|
||||
use std::io::{self, Stdin, stdin, stdout};
|
||||
use std::num::IntErrorKind;
|
||||
use std::os::fd::{AsFd, BorrowedFd};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use uucore::error::{UError, UResult, USimpleError, UUsageError};
|
||||
use uucore::error::{FromIo, UError, UResult, USimpleError, UUsageError};
|
||||
use uucore::format_usage;
|
||||
use uucore::parser::num_parser::ExtendedParser;
|
||||
use uucore::translate;
|
||||
@@ -124,12 +124,13 @@ struct Options<'a> {
|
||||
all: bool,
|
||||
save: bool,
|
||||
file: Device,
|
||||
device_name: String,
|
||||
settings: Option<Vec<&'a str>>,
|
||||
}
|
||||
|
||||
enum Device {
|
||||
File(File),
|
||||
Stdout(Stdout),
|
||||
Stdin(Stdin),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -166,7 +167,7 @@ impl AsFd for Device {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
match self {
|
||||
Self::File(f) => f.as_fd(),
|
||||
Self::Stdout(stdout) => stdout.as_fd(),
|
||||
Self::Stdin(stdin) => stdin.as_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -175,45 +176,42 @@ impl AsRawFd for Device {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
match self {
|
||||
Self::File(f) => f.as_raw_fd(),
|
||||
Self::Stdout(stdout) => stdout.as_raw_fd(),
|
||||
Self::Stdin(stdin) => stdin.as_raw_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Options<'a> {
|
||||
fn from(matches: &'a ArgMatches) -> io::Result<Self> {
|
||||
Ok(Self {
|
||||
all: matches.get_flag(options::ALL),
|
||||
save: matches.get_flag(options::SAVE),
|
||||
file: match matches.get_one::<String>(options::FILE) {
|
||||
// Two notes here:
|
||||
// 1. O_NONBLOCK is needed because according to GNU docs, a
|
||||
// POSIX tty can block waiting for carrier-detect if the
|
||||
// "clocal" flag is not set. If your TTY is not connected
|
||||
// to a modem, it is probably not relevant though.
|
||||
// 2. We never close the FD that we open here, but the OS
|
||||
// will clean up the FD for us on exit, so it doesn't
|
||||
// matter. The alternative would be to have an enum of
|
||||
// BorrowedFd/OwnedFd to handle both cases.
|
||||
Some(f) => Device::File(
|
||||
let (file, device_name) = match matches.get_one::<String>(options::FILE) {
|
||||
// Two notes here:
|
||||
// 1. O_NONBLOCK is needed because according to GNU docs, a
|
||||
// POSIX tty can block waiting for carrier-detect if the
|
||||
// "clocal" flag is not set. If your TTY is not connected
|
||||
// to a modem, it is probably not relevant though.
|
||||
// 2. We never close the FD that we open here, but the OS
|
||||
// will clean up the FD for us on exit, so it doesn't
|
||||
// matter. The alternative would be to have an enum of
|
||||
// BorrowedFd/OwnedFd to handle both cases.
|
||||
Some(f) => (
|
||||
Device::File(
|
||||
std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(O_NONBLOCK)
|
||||
.open(f)?,
|
||||
),
|
||||
// default to /dev/tty, if that does not exist then default to stdout
|
||||
None => {
|
||||
if let Ok(f) = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(O_NONBLOCK)
|
||||
.open("/dev/tty")
|
||||
{
|
||||
Device::File(f)
|
||||
} else {
|
||||
Device::Stdout(stdout())
|
||||
}
|
||||
}
|
||||
},
|
||||
f.clone(),
|
||||
),
|
||||
// Per POSIX, stdin is used for TTY operations when no device is specified.
|
||||
// This matches GNU coreutils behavior: if stdin is not a TTY,
|
||||
// tcgetattr will fail with "Inappropriate ioctl for device".
|
||||
None => (Device::Stdin(stdin()), "standard input".to_string()),
|
||||
};
|
||||
Ok(Self {
|
||||
all: matches.get_flag(options::ALL),
|
||||
save: matches.get_flag(options::SAVE),
|
||||
file,
|
||||
device_name,
|
||||
settings: matches
|
||||
.get_many::<String>(options::SETTINGS)
|
||||
.map(|v| v.map(|s| s.as_ref()).collect()),
|
||||
@@ -412,8 +410,8 @@ fn stty(opts: &Options) -> UResult<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Figure out the right error message for when tcgetattr fails
|
||||
let mut termios = tcgetattr(opts.file.as_fd())?;
|
||||
let mut termios =
|
||||
tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?;
|
||||
|
||||
// iterate over valid_args, match on the arg type, do the matching apply function
|
||||
for arg in &valid_args {
|
||||
@@ -433,8 +431,7 @@ fn stty(opts: &Options) -> UResult<()> {
|
||||
}
|
||||
tcsetattr(opts.file.as_fd(), set_arg, &termios)?;
|
||||
} else {
|
||||
// TODO: Figure out the right error message for when tcgetattr fails
|
||||
let termios = tcgetattr(opts.file.as_fd())?;
|
||||
let termios = tcgetattr(opts.file.as_fd()).map_err_context(|| opts.device_name.clone())?;
|
||||
print_settings(&termios, opts)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -997,7 +994,7 @@ fn apply_char_mapping(termios: &mut Termios, mapping: &(S, u8)) {
|
||||
///
|
||||
/// The state array contains:
|
||||
/// - `state[0]`: input flags
|
||||
/// - `state[1]`: output flags
|
||||
/// - `state[1]`: output flags
|
||||
/// - `state[2]`: control flags
|
||||
/// - `state[3]`: local flags
|
||||
/// - `state[4..]`: control characters (optional)
|
||||
|
||||
@@ -1557,6 +1557,76 @@ fn test_saved_state_with_control_chars() {
|
||||
.code_is(exp_result.code());
|
||||
}
|
||||
|
||||
// Per POSIX, stty uses stdin for TTY operations. When stdin is a pipe, it should fail.
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_stdin_not_tty_fails() {
|
||||
// ENOTTY error message varies by platform/libc:
|
||||
// - glibc: "Inappropriate ioctl for device"
|
||||
// - musl: "Not a tty"
|
||||
// - Android: "Not a typewriter"
|
||||
#[cfg(target_os = "android")]
|
||||
let expected_error = "standard input: Not a typewriter";
|
||||
#[cfg(all(not(target_os = "android"), target_env = "musl"))]
|
||||
let expected_error = "standard input: Not a tty";
|
||||
#[cfg(all(not(target_os = "android"), not(target_env = "musl")))]
|
||||
let expected_error = "standard input: Inappropriate ioctl for device";
|
||||
|
||||
new_ucmd!()
|
||||
.pipe_in("")
|
||||
.fails()
|
||||
.stderr_contains(expected_error);
|
||||
}
|
||||
|
||||
// Test that stty uses stdin for TTY operations per POSIX.
|
||||
// Verifies: output redirection (#8012), save/restore pattern (#8608), stdin redirection (#8848)
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_stty_uses_stdin() {
|
||||
use std::fs::File;
|
||||
use std::process::Stdio;
|
||||
|
||||
let (path, _controller, _replica) = pty_path();
|
||||
|
||||
// Output redirection: stty > file (stdin is still TTY)
|
||||
let stdin = File::open(&path).unwrap();
|
||||
new_ucmd!()
|
||||
.set_stdin(stdin)
|
||||
.set_stdout(Stdio::piped())
|
||||
.succeeds()
|
||||
.stdout_contains("speed");
|
||||
|
||||
// Save/restore: stty $(stty -g) pattern
|
||||
let stdin = File::open(&path).unwrap();
|
||||
let saved = new_ucmd!()
|
||||
.arg("-g")
|
||||
.set_stdin(stdin)
|
||||
.set_stdout(Stdio::piped())
|
||||
.succeeds()
|
||||
.stdout_str()
|
||||
.trim()
|
||||
.to_string();
|
||||
assert!(saved.contains(':'), "Expected colon-separated saved state");
|
||||
|
||||
let stdin = File::open(&path).unwrap();
|
||||
new_ucmd!().arg(&saved).set_stdin(stdin).succeeds();
|
||||
|
||||
// Stdin redirection: stty rows 30 cols 100 < /dev/pts/N
|
||||
let stdin = File::open(&path).unwrap();
|
||||
new_ucmd!()
|
||||
.args(&["rows", "30", "cols", "100"])
|
||||
.set_stdin(stdin)
|
||||
.succeeds();
|
||||
|
||||
let stdin = File::open(&path).unwrap();
|
||||
new_ucmd!()
|
||||
.arg("--all")
|
||||
.set_stdin(stdin)
|
||||
.succeeds()
|
||||
.stdout_contains("rows 30")
|
||||
.stdout_contains("columns 100");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_columns_env_wrapping() {
|
||||
|
||||
Reference in New Issue
Block a user