Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/cspell.dictionaries/workspace.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ istrip
litout
opost
parodd
ENOTTY

# translation tests
CLICOLOR
Expand Down
71 changes: 34 additions & 37 deletions src/uu/stty/src/stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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(),
}
}
}
Expand All @@ -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()),
Expand Down Expand Up @@ -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 {
Expand All @@ -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(())
Expand Down Expand Up @@ -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)
Expand Down
70 changes: 70 additions & 0 deletions tests/by-util/test_stty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading