summaryrefslogtreecommitdiff
path: root/corebinutils
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:29:35 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:29:35 +0300
commit1edb2cb7b8a720ba8ae4438947256515a93b7ca9 (patch)
tree7428ee326d8d1f00331258b038a33d675f7e0459 /corebinutils
parent3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7 (diff)
parentf59550da80c3bd763f1d9782b3b3a06f16826889 (diff)
downloadProject-Tick-1edb2cb7b8a720ba8ae4438947256515a93b7ca9.tar.gz
Project-Tick-1edb2cb7b8a720ba8ae4438947256515a93b7ca9.zip
Add 'corebinutils/stty/' from commit 'f59550da80c3bd763f1d9782b3b3a06f16826889'
git-subtree-dir: corebinutils/stty git-subtree-mainline: 3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7 git-subtree-split: f59550da80c3bd763f1d9782b3b3a06f16826889
Diffstat (limited to 'corebinutils')
-rw-r--r--corebinutils/stty/.gitignore25
-rw-r--r--corebinutils/stty/GNUmakefile35
-rw-r--r--corebinutils/stty/LICENSE29
-rw-r--r--corebinutils/stty/README.md46
-rw-r--r--corebinutils/stty/TODO1
-rw-r--r--corebinutils/stty/stty.1647
-rw-r--r--corebinutils/stty/stty.c1607
-rw-r--r--corebinutils/stty/tests/mkpty.c56
-rw-r--r--corebinutils/stty/tests/termios_probe.c226
-rw-r--r--corebinutils/stty/tests/test.sh343
10 files changed, 3015 insertions, 0 deletions
diff --git a/corebinutils/stty/.gitignore b/corebinutils/stty/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/stty/.gitignore
@@ -0,0 +1,25 @@
+*.a
+*.core
+*.lo
+*.nossppico
+*.o
+*.orig
+*.pico
+*.pieo
+*.po
+*.rej
+*.so
+*.so.[0-9]*
+*.sw[nop]
+*~
+.*DS_Store
+.cache
+.clangd
+.ccls-cache
+.depend*
+compile_commands.json
+compile_commands.events.json
+tags
+build/
+out/
+.linux-obj/
diff --git a/corebinutils/stty/GNUmakefile b/corebinutils/stty/GNUmakefile
new file mode 100644
index 0000000000..31f01edfae
--- /dev/null
+++ b/corebinutils/stty/GNUmakefile
@@ -0,0 +1,35 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS += -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/stty
+OBJS := $(OBJDIR)/stty.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/stty.o: $(CURDIR)/stty.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/stty.c" -o "$@"
+
+test: $(TARGET)
+ CC="$(CC)" STTY_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/stty/LICENSE b/corebinutils/stty/LICENSE
new file mode 100644
index 0000000000..7cc54b0acd
--- /dev/null
+++ b/corebinutils/stty/LICENSE
@@ -0,0 +1,29 @@
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+This code is derived from software contributed to Berkeley by
+the Institute of Electrical and Electronics Engineers, Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/corebinutils/stty/README.md b/corebinutils/stty/README.md
new file mode 100644
index 0000000000..b371a36dcf
--- /dev/null
+++ b/corebinutils/stty/README.md
@@ -0,0 +1,46 @@
+# stty
+
+Linux-native `stty` port of the FreeBSD userland tool, built for Linux + musl without a BSD shim layer.
+
+## Build
+
+```sh
+make -C bin/stty
+make -C bin/stty clean all CC=musl-gcc
+```
+
+## Test
+
+```sh
+make -C bin/stty test
+make -C bin/stty clean test CC=musl-gcc
+```
+
+The test suite creates its own pseudoterminal, verifies termios and winsize state through independent helpers, and exercises negative paths as well as round-trip `-g` restore behavior.
+
+## Port strategy
+
+- Replaced the FreeBSD multi-file implementation with a single Linux-native termios/winsize implementation.
+- Kept `stty.1` as the semantic contract and mapped documented behavior directly onto Linux APIs:
+ - `tcgetattr(3)` / `tcsetattr(3)` for termios state
+ - `cfsetispeed(3)` / `cfsetospeed(3)` for baud rates
+ - `TIOCGWINSZ` / `TIOCSWINSZ` for `size`, `rows`, and `columns`
+ - `TIOCGETD` / `TIOCSETD` with `N_TTY` for `tty`
+ - `EXTPROC` local flag for `extproc`
+- Avoided GNU-specific behavior; the build only enables `_DEFAULT_SOURCE` and `_XOPEN_SOURCE=700`.
+
+## Linux-supported and unsupported semantics
+
+Supported:
+
+- POSIX and BSD output modes: default, `-a`, `-e`, `-g`
+- Linux termios flags and compatibility aliases documented in `stty.1`
+- `speed`, `ispeed`, `ospeed`, bare numeric baud-rate arguments, `raw`, `-raw`, `sane`, `cbreak`, `dec`, `tty`, `rows`, `columns`, `size`
+
+Explicitly unsupported with hard errors:
+
+- `altwerase`, `mdmbuf`, `rtsdtr`
+- `kerninfo` / `nokerninfo`
+- control characters `status`, `dsusp`, `erase2`
+- `ek`, because Linux has no `VERASE2` and partial emulation would silently change semantics
+- arbitrary non-table baud rates such as `12345`; this port intentionally stays on the stable Linux termios API instead of silently inventing `termios2` behavior
diff --git a/corebinutils/stty/TODO b/corebinutils/stty/TODO
new file mode 100644
index 0000000000..adff0a5eb3
--- /dev/null
+++ b/corebinutils/stty/TODO
@@ -0,0 +1 @@
+1) Please test in VM
diff --git a/corebinutils/stty/stty.1 b/corebinutils/stty/stty.1
new file mode 100644
index 0000000000..7baca89b2b
--- /dev/null
+++ b/corebinutils/stty/stty.1
@@ -0,0 +1,647 @@
+.\"-
+.\" Copyright (c) 1990, 1993, 1994
+.\" The Regents of the University of California. All rights reserved.
+.\"
+.\" Copyright (c) 2026 Project Tick. All rights reserved.
+.\"
+.\" This code is derived from software contributed to Berkeley by
+.\" the Institute of Electrical and Electronics Engineers, Inc.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\" 3. Neither the name of the University nor the names of its contributors
+.\" may be used to endorse or promote products derived from this software
+.\" without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.Dd September 27, 2022
+.Dt STTY 1
+.Os
+.Sh NAME
+.Nm stty
+.Nd set the options for a terminal device interface
+.Sh SYNOPSIS
+.Nm
+.Op Fl a | e | g
+.Op Fl f Ar file
+.Op Ar arguments
+.Sh DESCRIPTION
+The
+.Nm
+utility sets or reports on terminal
+characteristics for the device that is its standard input.
+If no options or arguments are specified, it reports the settings of a subset
+of characteristics as well as additional ones if they differ from their
+default values.
+Otherwise it modifies
+the terminal state according to the specified arguments.
+Some combinations of arguments are mutually
+exclusive on some terminal types.
+.Pp
+The following options are available:
+.Bl -tag -width indent
+.It Fl a
+Display all the current settings for the terminal to standard output
+as per
+.St -p1003.2 .
+.It Fl e
+Display all the current settings for the terminal to standard output
+in the traditional
+.Bx
+``all'' and ``everything'' formats.
+.It Fl f
+Open and use the terminal named by
+.Ar file
+rather than using standard input.
+The file is opened
+using the
+.Dv O_NONBLOCK
+flag of
+.Fn open ,
+making it possible to
+set or display settings on a terminal that might otherwise
+block on the open.
+.It Fl g
+Display all the current settings for the terminal to standard output
+in a form that may be used as an argument to a subsequent invocation of
+.Nm
+to restore the current terminal state as per
+.St -p1003.2 .
+.El
+.Pp
+The following arguments are available to set the terminal
+characteristics:
+.Ss Control Modes:
+Control mode flags affect hardware characteristics associated with the
+terminal.
+This corresponds to the c_cflag in the termios structure.
+.Bl -tag -width Fl
+.It Cm parenb Pq Fl parenb
+Enable (disable) parity generation
+and detection.
+.It Cm parodd Pq Fl parodd
+Select odd (even) parity.
+.It Cm cs5 cs6 cs7 cs8
+Select character size, if possible.
+.It Ar number
+Set terminal baud rate to the
+number given, if possible.
+If the
+baud rate is set to zero, modem
+control is no longer
+asserted.
+.It Cm ispeed Ar number
+Set terminal input baud rate to the
+number given, if possible.
+If the
+input baud rate is set to zero, the
+input baud rate is set to the
+value of the output baud
+rate.
+.It Cm ospeed Ar number
+Set terminal output baud rate to
+the number given, if possible.
+If
+the output baud rate is set to
+zero, modem control is
+no longer asserted.
+.It Cm speed Ar number
+This sets both
+.Cm ispeed
+and
+.Cm ospeed
+to
+.Ar number .
+.It Cm hupcl Pq Fl hupcl
+Stop asserting modem control
+(do not stop asserting modem control) on last close.
+.It Cm hup Pq Fl hup
+Same as hupcl
+.Pq Fl hupcl .
+.It Cm cstopb Pq Fl cstopb
+Use two (one) stop bits per character.
+.It Cm cread Pq Fl cread
+Enable (disable) the receiver.
+.It Cm clocal Pq Fl clocal
+Assume a line without (with) modem
+control.
+.It Cm crtscts Pq Fl crtscts
+Enable (disable) RTS/CTS flow control.
+.It Cm rtsdtr Pq Fl rtsdtr
+Enable (disable) asserting RTS/DTR on open.
+.El
+.Ss Input Modes:
+This corresponds to the c_iflag in the termios structure.
+.Bl -tag -width Fl
+.It Cm ignbrk Pq Fl ignbrk
+Ignore (do not ignore) break on
+input.
+.It Cm brkint Pq Fl brkint
+Signal (do not signal)
+.Dv INTR
+on
+break.
+.It Cm ignpar Pq Fl ignpar
+Ignore (do not ignore) characters with parity
+errors.
+.It Cm parmrk Pq Fl parmrk
+Mark (do not mark) characters with parity errors.
+.It Cm inpck Pq Fl inpck
+Enable (disable) input parity
+checking.
+.It Cm istrip Pq Fl istrip
+Strip (do not strip) input characters
+to seven bits.
+.It Cm inlcr Pq Fl inlcr
+Map (do not map)
+.Dv NL
+to
+.Dv CR
+on input.
+.It Cm igncr Pq Fl igncr
+Ignore (do not ignore)
+.Dv CR
+on input.
+.It Cm icrnl Pq Fl icrnl
+Map (do not map)
+.Dv CR
+to
+.Dv NL
+on input.
+.It Cm ixon Pq Fl ixon
+Enable (disable)
+.Dv START/STOP
+output
+control.
+Output from the system is
+stopped when the system receives
+.Dv STOP
+and started when the system
+receives
+.Dv START ,
+or if
+.Cm ixany
+is set, any character restarts output.
+.It Cm ixoff Pq Fl ixoff
+Request that the system send (not
+send)
+.Dv START/STOP
+characters when
+the input queue is nearly
+empty/full.
+.It Cm ixany Pq Fl ixany
+Allow any character (allow only
+.Dv START )
+to restart output.
+.It Cm imaxbel Pq Fl imaxbel
+The system imposes a limit of
+.Dv MAX_INPUT
+(currently 255) characters in the input queue.
+If
+.Cm imaxbel
+is set and the input queue limit has been reached,
+subsequent input causes the system to send an ASCII BEL
+character to the output queue (the terminal beeps at you).
+Otherwise,
+if
+.Cm imaxbel
+is unset and the input queue is full, the next input character causes
+the entire input and output queues to be discarded.
+.It Cm iutf8 Pq Fl iutf8
+Assume that input characters are UTF-8 encoded. Setting this flag
+causes backspace to properly delete multibyte characters in canonical mode.
+.El
+.Ss Output Modes:
+This corresponds to the c_oflag of the termios structure.
+.Bl -tag -width Fl
+.It Cm opost Pq Fl opost
+Post-process output (do not
+post-process output; ignore all other
+output modes).
+.It Cm onlcr Pq Fl onlcr
+Map (do not map)
+.Dv NL
+to
+.Dv CR-NL
+on output.
+.It Cm ocrnl Pq Fl ocrnl
+Map (do not map)
+.Dv CR
+to
+.Dv NL
+on output.
+.It Cm tab0 tab3
+Select tab expansion policy.
+.Cm tab0
+disables tab expansion, while
+.Cm tab3
+enables it.
+.It Cm onocr Pq Fl onocr
+Do not (do) output CRs at column zero.
+.It Cm onlret Pq Fl onlret
+On the terminal NL performs (does not perform) the CR function.
+.El
+.Ss Local Modes:
+Local mode flags (lflags) affect various and sundry characteristics of terminal
+processing.
+Historically the term "local" pertained to new job control features
+implemented by Jim Kulp on a
+.Tn Pdp 11/70
+at
+.Tn IIASA .
+Later the driver ran on the first
+.Tn VAX
+at Evans Hall, UC Berkeley, where the job control details
+were greatly modified but the structure definitions and names
+remained essentially unchanged.
+The second interpretation of the 'l' in lflag
+is ``line discipline flag'' which corresponds to the
+.Ar c_lflag
+of the
+.Ar termios
+structure.
+.Bl -tag -width Fl
+.It Cm isig Pq Fl isig
+Enable (disable) the checking of
+characters against the special control
+characters
+.Dv INTR , QUIT ,
+and
+.Dv SUSP .
+.It Cm icanon Pq Fl icanon
+Enable (disable) canonical input
+.Pf ( Dv ERASE
+and
+.Dv KILL
+processing).
+.It Cm iexten Pq Fl iexten
+Enable (disable) any implementation
+defined special control characters
+not currently controlled by icanon,
+isig, or ixon.
+.It Cm echo Pq Fl echo
+Echo back (do not echo back) every
+character typed.
+.It Cm echoe Pq Fl echoe
+The
+.Dv ERASE
+character shall (shall
+not) visually erase the last character
+in the current line from the
+display, if possible.
+.It Cm echok Pq Fl echok
+Echo (do not echo)
+.Dv NL
+after
+.Dv KILL
+character.
+.It Cm echoke Pq Fl echoke
+The
+.Dv KILL
+character shall (shall
+not) visually erase the
+current line from the
+display, if possible.
+.It Cm echonl Pq Fl echonl
+Echo (do not echo)
+.Dv NL ,
+even if echo
+is disabled.
+.It Cm echoctl Pq Fl echoctl
+If
+.Cm echoctl
+is set, echo control characters as ^X.
+Otherwise control characters
+echo as themselves.
+.It Cm echoprt Pq Fl echoprt
+For printing terminals.
+If set, echo erased characters backwards within ``\\''
+and ``/''.
+Otherwise, disable this feature.
+.It Cm noflsh Pq Fl noflsh
+Disable (enable) flush after
+.Dv INTR , QUIT , SUSP .
+.It Cm tostop Pq Fl tostop
+Send (do not send)
+.Dv SIGTTOU
+for background output.
+This causes background jobs to stop if they attempt
+terminal output.
+.It Cm altwerase Pq Fl altwerase
+Use (do not use) an alternate word erase algorithm when processing
+.Dv WERASE
+characters.
+This alternate algorithm considers sequences of
+alphanumeric/underscores as words.
+It also skips the first preceding character in its classification
+(as a convenience since the one preceding character could have been
+erased with simply an
+.Dv ERASE
+character.)
+.It Cm mdmbuf Pq Fl mdmbuf
+If set, flow control output based on condition of Carrier Detect.
+Otherwise
+writes return an error if Carrier Detect is low (and Carrier is not being
+ignored with the
+.Dv CLOCAL
+flag.)
+.It Cm flusho Pq Fl flusho
+Indicates output is (is not) being discarded.
+.It Cm pendin Pq Fl pendin
+Indicates input is (is not) pending after a switch from non-canonical
+to canonical mode and will be re-input when a read becomes pending
+or more input arrives.
+.El
+.Ss Control Characters:
+.Bl -tag -width Fl
+.It Ar control-character Ar string
+Set
+.Ar control-character
+to
+.Ar string .
+If string is a single character,
+the control character is set to
+that character.
+If string is the
+two character sequence "^-" or the
+string "undef" the control character
+is disabled (i.e., set to
+.Pf { Dv _POSIX_VDISABLE Ns } . )
+.Pp
+Recognized control-characters:
+.Bd -ragged -offset indent
+.Bl -column character Subscript
+.It control- Ta \& Ta \&
+.It character Ta Subscript Ta Description
+.It _________ Ta _________ Ta _______________
+.It eof Ta Tn VEOF Ta EOF No character
+.It eol Ta Tn VEOL Ta EOL No character
+.It eol2 Ta Tn VEOL2 Ta EOL2 No character
+.It erase Ta Tn VERASE Ta ERASE No character
+.It erase2 Ta Tn VERASE2 Ta ERASE2 No character
+.It werase Ta Tn VWERASE Ta WERASE No character
+.It intr Ta Tn VINTR Ta INTR No character
+.It kill Ta Tn VKILL Ta KILL No character
+.It quit Ta Tn VQUIT Ta QUIT No character
+.It susp Ta Tn VSUSP Ta SUSP No character
+.It start Ta Tn VSTART Ta START No character
+.It stop Ta Tn VSTOP Ta STOP No character
+.It dsusp Ta Tn VDSUSP Ta DSUSP No character
+.It lnext Ta Tn VLNEXT Ta LNEXT No character
+.It reprint Ta Tn VREPRINT Ta REPRINT No character
+.It status Ta Tn VSTATUS Ta STATUS No character
+.El
+.Ed
+.It Cm min Ar number
+.It Cm time Ar number
+Set the value of min or time to
+number.
+.Dv MIN
+and
+.Dv TIME
+are used in
+Non-Canonical mode input processing
+(-icanon).
+.El
+.Ss Combination Modes:
+.Bl -tag -width Fl
+.It Ar saved settings
+Set the current terminal
+characteristics to the saved settings
+produced by the
+.Fl g
+option.
+.It Cm evenp No or Cm parity
+Enable parenb and cs7; disable
+parodd.
+.It Cm oddp
+Enable parenb, cs7, and parodd.
+.It Fl parity , evenp , oddp
+Disable parenb, and set cs8.
+.It Cm \&nl Pq Fl \&nl
+Enable (disable) icrnl.
+In addition
+-nl unsets inlcr and igncr.
+.It Cm ek
+Reset
+.Dv ERASE ,
+.Dv ERASE2 ,
+and
+.Dv KILL
+characters
+back to system defaults.
+.It Cm sane
+Resets all modes to reasonable values for interactive terminal use.
+.It Cm tty
+Set the line discipline to the standard terminal line discipline
+.Dv TTYDISC .
+.It Cm crt Pq Fl crt
+Set (disable) all modes suitable for a CRT display device.
+.It Cm kerninfo Pq Fl kerninfo
+Enable (disable) the system generated status line associated with
+processing a
+.Dv STATUS
+character (usually set to ^T).
+The status line consists of the
+system load average, the current command name, its process ID, the
+event the process is waiting on (or the status of the process), the user
+and system times, percent cpu, and current memory usage.
+.Pp
+If the
+.Xr sysctl 8
+variable
+.Va kern.tty_info_kstacks
+is set to a non-zero value, the status message also includes the kernel program
+stack of the foreground thread.
+.It Cm columns Ar number
+The terminal size is recorded as having
+.Ar number
+columns.
+.It Cm cols Ar number
+is an alias for
+.Cm columns .
+.It Cm rows Ar number
+The terminal size is recorded as having
+.Ar number
+rows.
+.It Cm dec
+Set modes suitable for users of Digital Equipment Corporation systems
+.Dv ( ERASE ,
+.Dv KILL ,
+and
+.Dv INTR
+characters are set to ^?, ^U, and ^C;
+.Dv ixany
+is disabled, and
+.Dv crt
+is enabled.)
+.It Cm extproc Pq Fl extproc
+If set, this flag indicates that some amount of terminal processing is being
+performed by either the terminal hardware or by the remote side connected
+to a pty.
+.It Cm raw Pq Fl raw
+If set, change the modes of the terminal so that no input or output processing
+is performed.
+If unset, change the modes of the terminal to some reasonable
+state that performs input and output processing.
+Note that since the
+terminal driver no longer has a single
+.Dv RAW
+bit, it is not possible to intuit what flags were set prior to setting
+.Cm raw .
+This means that unsetting
+.Cm raw
+may not put back all the setting that were previously in effect.
+To set the terminal into a raw state and then accurately restore it, the following
+shell code is recommended:
+.Bd -literal
+save_state=$(stty -g)
+stty raw
+\&...
+stty "$save_state"
+.Ed
+.It Cm size
+The size of the terminal is printed as two numbers on a single line,
+first rows, then columns.
+.El
+.Ss Compatibility Modes:
+These modes remain for compatibility with the previous version of
+the
+.Nm
+command.
+.Bl -tag -width Fl
+.It Cm all
+Reports all the terminal modes as with
+.Cm stty Fl a
+except that the control characters are printed in a columnar format.
+.It Cm everything
+Same as
+.Cm all .
+.It Cm cooked
+Same as
+.Cm sane .
+.It Cm cbreak
+If set, enables
+.Cm brkint , ixon , imaxbel , opost ,
+.Cm isig , iexten ,
+and
+.Fl icanon .
+If unset, same as
+.Cm sane .
+.It Cm new
+Same as
+.Cm tty .
+.It Cm old
+Same as
+.Cm tty .
+.It Cm newcrt Pq Fl newcrt
+Same as
+.Cm crt .
+.It Cm pass8
+The converse of
+.Cm parity .
+.It Cm tandem Pq Fl tandem
+Same as
+.Cm ixoff .
+.It Cm decctlq Pq Fl decctlq
+The converse of
+.Cm ixany .
+.It Cm crterase Pq Fl crterase
+Same as
+.Cm echoe .
+.It Cm crtbs Pq Fl crtbs
+Same as
+.Cm echoe .
+.It Cm crtkill Pq Fl crtkill
+Same as
+.Cm echoke .
+.It Cm ctlecho Pq Fl ctlecho
+Same as
+.Cm echoctl .
+.It Cm prterase Pq Fl prterase
+Same as
+.Cm echoprt .
+.It Cm litout Pq Fl litout
+The converse of
+.Cm opost .
+.It Cm oxtabs Pq Fl oxtabs
+Expand (do not expand) tabs to spaces on output.
+.It Cm tabs Pq Fl tabs
+The converse of
+.Cm oxtabs .
+.It Cm brk Ar value
+Same as the control character
+.Cm eol .
+.It Cm flush Ar value
+Same as the control character
+.Cm discard .
+.It Cm rprnt Ar value
+Same as the control character
+.Cm reprint .
+.El
+.Sh INTERACTION WITH JOB CONTROL
+Modifications to the terminal settings are treated by job control
+(see
+.Xr termios 4 )
+same as writes.
+When the
+.Nm
+utility is executing in a background process group,
+such attempts result in the kernel sending the
+.Dv SIGTTOU
+signal and stopping the process until its group is returned
+to foreground.
+The non-blocking open of the terminal device with the
+.Fl f
+option to
+.Nm
+does not affect the behavior.
+If it is desirable to modify the settings from the background,
+.Xr sh 1
+users might utilize the following idiom:
+.Bd -literal
+(trap '' TTOU; stty -f /dev/tty sane)
+.Ed
+.Pp
+Note that changing terminal settings for a running foreground
+job that is not prepared for it might cause inconsistencies.
+.Sh EXIT STATUS
+.Ex -std
+.Sh SEE ALSO
+.Xr resizewin 1 ,
+.Xr termios 4 ,
+.Xr pstat 8
+.Sh STANDARDS
+The
+.Nm
+utility is expected to be
+.St -p1003.2
+compatible.
+The flags
+.Fl e
+and
+.Fl f
+are
+extensions to the standard.
+.Sh HISTORY
+A
+.Nm
+command appeared in
+.At v2 .
diff --git a/corebinutils/stty/stty.c b/corebinutils/stty/stty.c
new file mode 100644
index 0000000000..538756a462
--- /dev/null
+++ b/corebinutils/stty/stty.c
@@ -0,0 +1,1607 @@
+/*
+
+SPDX-License-Identifier: BSD-3-Clause
+
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+This code is derived from software contributed to Berkeley by
+the Institute of Electrical and Electronics Engineers, Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+
+*/
+
+#define _DEFAULT_SOURCE 1
+#define _XOPEN_SOURCE 700
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdnoreturn.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#ifndef N_TTY
+#define N_TTY 0
+#endif
+
+#ifndef CTRL
+#define CTRL(x) ((x) & 037)
+#endif
+#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
+#define OUTPUT_WIDTH 72
+
+#ifndef CINTR
+#define CINTR CTRL('c')
+#endif
+#ifndef CQUIT
+#define CQUIT 034
+#endif
+#ifndef CERASE
+#define CERASE 0177
+#endif
+#ifndef CKILL
+#define CKILL CTRL('u')
+#endif
+#ifndef CEOF
+#define CEOF CTRL('d')
+#endif
+#ifndef CEOL
+#define CEOL _POSIX_VDISABLE
+#endif
+#ifndef CSUSP
+#define CSUSP CTRL('z')
+#endif
+#ifndef CSTART
+#define CSTART CTRL('q')
+#endif
+#ifndef CSTOP
+#define CSTOP CTRL('s')
+#endif
+#ifndef CDISCARD
+#define CDISCARD CTRL('o')
+#endif
+#ifndef CLNEXT
+#define CLNEXT CTRL('v')
+#endif
+#ifndef CREPRINT
+#define CREPRINT CTRL('r')
+#endif
+#ifndef CWERASE
+#define CWERASE CTRL('w')
+#endif
+
+enum output_format {
+ FORMAT_DEFAULT,
+ FORMAT_BSD,
+ FORMAT_POSIX,
+ FORMAT_GFMT,
+};
+
+enum termios_field {
+ FIELD_IFLAG,
+ FIELD_OFLAG,
+ FIELD_CFLAG,
+ FIELD_LFLAG,
+};
+
+struct state {
+ int fd;
+ bool close_fd;
+ const char *label;
+ struct termios termios;
+ struct termios sane;
+ struct winsize winsize;
+ bool have_winsize;
+ bool termios_dirty;
+ bool winsize_dirty;
+ int line_discipline;
+ bool have_line_discipline;
+};
+
+struct speed_map {
+ unsigned int baud;
+ speed_t symbol;
+};
+
+struct flag_action {
+ const char *name;
+ enum termios_field field;
+ tcflag_t set_mask;
+ tcflag_t clear_mask;
+};
+
+struct print_flag {
+ const char *off_name;
+ enum termios_field field;
+ tcflag_t mask;
+};
+
+struct control_char {
+ const char *name;
+ size_t index;
+ cc_t default_value;
+ bool numeric;
+};
+
+struct named_command {
+ const char *name;
+ bool allow_off;
+ bool needs_argument;
+ enum {
+ CMD_PRINT_BSD,
+ CMD_SANE,
+ CMD_CBREAK,
+ CMD_COLUMNS,
+ CMD_ROWS,
+ CMD_SIZE,
+ CMD_SPEED,
+ CMD_ISPEED,
+ CMD_OSPEED,
+ CMD_DEC,
+ CMD_TTY,
+ CMD_NL,
+ CMD_RAW,
+ CMD_EK
+ } id;
+};
+
+struct line_builder {
+ const char *label;
+ size_t column;
+};
+
+static noreturn void usage(void);
+static noreturn void usage_error(const char *fmt, ...);
+static noreturn void die(const char *fmt, ...);
+static noreturn void die_errno(const char *fmt, ...);
+static noreturn void die_unsupported(const char *name, const char *reason);
+
+static void init_state(struct state *state, const char *path);
+static void make_sane_termios(struct termios *sane, const struct termios *current);
+static void apply_named_argument(struct state *state, char **argv, int argc, int *index);
+static bool maybe_apply_gfmt(struct state *state, const char *argument);
+static bool maybe_apply_flag_action(struct state *state, const char *argument);
+static bool maybe_apply_command(struct state *state, char **argv, int argc, int *index);
+static bool maybe_apply_control_char(struct state *state, char **argv, int argc, int *index);
+static bool maybe_apply_speed_token(struct state *state, const char *argument);
+static void print_state(const struct state *state, enum output_format format);
+static void print_boolean_group(const struct state *state, enum output_format format,
+ const char *label, const struct print_flag *flags, size_t count);
+static void print_control_chars(const struct state *state, enum output_format format);
+static void line_builder_begin(struct line_builder *builder, const char *label);
+static void line_builder_put(struct line_builder *builder, const char *token);
+static void line_builder_finish(struct line_builder *builder);
+static tcflag_t *field_ptr(struct termios *termios, enum termios_field field);
+static tcflag_t field_value(const struct termios *termios, enum termios_field field);
+static const struct control_char *find_control_char(const char *name);
+static const struct named_command *find_command(const char *name);
+static const char *unsupported_reason(const char *name);
+static void set_rows_or_columns(unsigned short *slot, const char *keyword, const char *value);
+static void set_speed(struct state *state, const char *keyword, const char *value,
+ bool set_input, bool set_output);
+static speed_t lookup_speed_symbol(unsigned int baud, bool *found);
+static unsigned int speed_to_baud(speed_t symbol);
+static unsigned long long parse_unsigned(const char *text, int base,
+ unsigned long long max_value, const char *what);
+static bool is_decimal_number(const char *text);
+static cc_t parse_control_char_value(const struct control_char *control, const char *value);
+static const char *format_control_char(const struct control_char *control, cc_t value,
+ char *buffer, size_t buffer_size);
+static void apply_sane(struct state *state);
+static void apply_cbreak(struct state *state, bool off);
+static void apply_dec(struct state *state);
+static void apply_nl(struct state *state, bool off);
+static void apply_raw(struct state *state, bool off);
+static void apply_tty(struct state *state);
+static void apply_ek(struct state *state);
+
+static const struct speed_map speed_table[] = {
+#ifdef B0
+ { 0U, B0 },
+#endif
+#ifdef B50
+ { 50U, B50 },
+#endif
+#ifdef B75
+ { 75U, B75 },
+#endif
+#ifdef B110
+ { 110U, B110 },
+#endif
+#ifdef B134
+ { 134U, B134 },
+#endif
+#ifdef B150
+ { 150U, B150 },
+#endif
+#ifdef B200
+ { 200U, B200 },
+#endif
+#ifdef B300
+ { 300U, B300 },
+#endif
+#ifdef B600
+ { 600U, B600 },
+#endif
+#ifdef B1200
+ { 1200U, B1200 },
+#endif
+#ifdef B1800
+ { 1800U, B1800 },
+#endif
+#ifdef B2400
+ { 2400U, B2400 },
+#endif
+#ifdef B4800
+ { 4800U, B4800 },
+#endif
+#ifdef B9600
+ { 9600U, B9600 },
+#endif
+#ifdef B19200
+ { 19200U, B19200 },
+#endif
+#ifdef B38400
+ { 38400U, B38400 },
+#endif
+#ifdef B57600
+ { 57600U, B57600 },
+#endif
+#ifdef B115200
+ { 115200U, B115200 },
+#endif
+#ifdef B230400
+ { 230400U, B230400 },
+#endif
+#ifdef B460800
+ { 460800U, B460800 },
+#endif
+#ifdef B500000
+ { 500000U, B500000 },
+#endif
+#ifdef B576000
+ { 576000U, B576000 },
+#endif
+#ifdef B921600
+ { 921600U, B921600 },
+#endif
+#ifdef B1000000
+ { 1000000U, B1000000 },
+#endif
+#ifdef B1152000
+ { 1152000U, B1152000 },
+#endif
+#ifdef B1500000
+ { 1500000U, B1500000 },
+#endif
+#ifdef B2000000
+ { 2000000U, B2000000 },
+#endif
+#ifdef B2500000
+ { 2500000U, B2500000 },
+#endif
+#ifdef B3000000
+ { 3000000U, B3000000 },
+#endif
+#ifdef B3500000
+ { 3500000U, B3500000 },
+#endif
+#ifdef B4000000
+ { 4000000U, B4000000 },
+#endif
+};
+
+static const struct flag_action flag_actions[] = {
+ { "cs5", FIELD_CFLAG, CS5, CSIZE },
+ { "cs6", FIELD_CFLAG, CS6, CSIZE },
+ { "cs7", FIELD_CFLAG, CS7, CSIZE },
+ { "cs8", FIELD_CFLAG, CS8, CSIZE },
+ { "cstopb", FIELD_CFLAG, CSTOPB, 0 },
+ { "-cstopb", FIELD_CFLAG, 0, CSTOPB },
+ { "cread", FIELD_CFLAG, CREAD, 0 },
+ { "-cread", FIELD_CFLAG, 0, CREAD },
+ { "parenb", FIELD_CFLAG, PARENB, 0 },
+ { "-parenb", FIELD_CFLAG, 0, PARENB },
+ { "parodd", FIELD_CFLAG, PARODD, 0 },
+ { "-parodd", FIELD_CFLAG, 0, PARODD },
+ { "parity", FIELD_CFLAG, PARENB | CS7, PARODD | CSIZE },
+ { "-parity", FIELD_CFLAG, CS8, PARENB | PARODD | CSIZE },
+ { "evenp", FIELD_CFLAG, PARENB | CS7, PARODD | CSIZE },
+ { "-evenp", FIELD_CFLAG, CS8, PARENB | PARODD | CSIZE },
+ { "oddp", FIELD_CFLAG, PARENB | PARODD | CS7, CSIZE },
+ { "-oddp", FIELD_CFLAG, CS8, PARENB | PARODD | CSIZE },
+ { "pass8", FIELD_CFLAG, CS8, PARENB | PARODD | CSIZE },
+ { "hupcl", FIELD_CFLAG, HUPCL, 0 },
+ { "-hupcl", FIELD_CFLAG, 0, HUPCL },
+ { "hup", FIELD_CFLAG, HUPCL, 0 },
+ { "-hup", FIELD_CFLAG, 0, HUPCL },
+ { "clocal", FIELD_CFLAG, CLOCAL, 0 },
+ { "-clocal", FIELD_CFLAG, 0, CLOCAL },
+#ifdef CRTSCTS
+ { "crtscts", FIELD_CFLAG, CRTSCTS, 0 },
+ { "-crtscts", FIELD_CFLAG, 0, CRTSCTS },
+#endif
+
+ { "ignbrk", FIELD_IFLAG, IGNBRK, 0 },
+ { "-ignbrk", FIELD_IFLAG, 0, IGNBRK },
+ { "brkint", FIELD_IFLAG, BRKINT, 0 },
+ { "-brkint", FIELD_IFLAG, 0, BRKINT },
+ { "ignpar", FIELD_IFLAG, IGNPAR, 0 },
+ { "-ignpar", FIELD_IFLAG, 0, IGNPAR },
+ { "parmrk", FIELD_IFLAG, PARMRK, 0 },
+ { "-parmrk", FIELD_IFLAG, 0, PARMRK },
+ { "inpck", FIELD_IFLAG, INPCK, 0 },
+ { "-inpck", FIELD_IFLAG, 0, INPCK },
+ { "istrip", FIELD_IFLAG, ISTRIP, 0 },
+ { "-istrip", FIELD_IFLAG, 0, ISTRIP },
+ { "inlcr", FIELD_IFLAG, INLCR, 0 },
+ { "-inlcr", FIELD_IFLAG, 0, INLCR },
+ { "igncr", FIELD_IFLAG, IGNCR, 0 },
+ { "-igncr", FIELD_IFLAG, 0, IGNCR },
+ { "icrnl", FIELD_IFLAG, ICRNL, 0 },
+ { "-icrnl", FIELD_IFLAG, 0, ICRNL },
+ { "ixon", FIELD_IFLAG, IXON, 0 },
+ { "-ixon", FIELD_IFLAG, 0, IXON },
+ { "ixoff", FIELD_IFLAG, IXOFF, 0 },
+ { "-ixoff", FIELD_IFLAG, 0, IXOFF },
+ { "tandem", FIELD_IFLAG, IXOFF, 0 },
+ { "-tandem", FIELD_IFLAG, 0, IXOFF },
+ { "ixany", FIELD_IFLAG, IXANY, 0 },
+ { "-ixany", FIELD_IFLAG, 0, IXANY },
+ { "decctlq", FIELD_IFLAG, 0, IXANY },
+ { "-decctlq", FIELD_IFLAG, IXANY, 0 },
+#ifdef IMAXBEL
+ { "imaxbel", FIELD_IFLAG, IMAXBEL, 0 },
+ { "-imaxbel", FIELD_IFLAG, 0, IMAXBEL },
+#endif
+#ifdef IUTF8
+ { "iutf8", FIELD_IFLAG, IUTF8, 0 },
+ { "-iutf8", FIELD_IFLAG, 0, IUTF8 },
+#endif
+
+ { "echo", FIELD_LFLAG, ECHO, 0 },
+ { "-echo", FIELD_LFLAG, 0, ECHO },
+ { "echoe", FIELD_LFLAG, ECHOE, 0 },
+ { "-echoe", FIELD_LFLAG, 0, ECHOE },
+ { "crterase", FIELD_LFLAG, ECHOE, 0 },
+ { "-crterase", FIELD_LFLAG, 0, ECHOE },
+ { "crtbs", FIELD_LFLAG, ECHOE, 0 },
+ { "-crtbs", FIELD_LFLAG, 0, ECHOE },
+ { "echok", FIELD_LFLAG, ECHOK, 0 },
+ { "-echok", FIELD_LFLAG, 0, ECHOK },
+#ifdef ECHOKE
+ { "echoke", FIELD_LFLAG, ECHOKE, 0 },
+ { "-echoke", FIELD_LFLAG, 0, ECHOKE },
+ { "crtkill", FIELD_LFLAG, ECHOKE, 0 },
+ { "-crtkill", FIELD_LFLAG, 0, ECHOKE },
+#endif
+ { "iexten", FIELD_LFLAG, IEXTEN, 0 },
+ { "-iexten", FIELD_LFLAG, 0, IEXTEN },
+ { "echonl", FIELD_LFLAG, ECHONL, 0 },
+ { "-echonl", FIELD_LFLAG, 0, ECHONL },
+#ifdef ECHOCTL
+ { "echoctl", FIELD_LFLAG, ECHOCTL, 0 },
+ { "-echoctl", FIELD_LFLAG, 0, ECHOCTL },
+ { "ctlecho", FIELD_LFLAG, ECHOCTL, 0 },
+ { "-ctlecho", FIELD_LFLAG, 0, ECHOCTL },
+#endif
+#ifdef ECHOPRT
+ { "echoprt", FIELD_LFLAG, ECHOPRT, 0 },
+ { "-echoprt", FIELD_LFLAG, 0, ECHOPRT },
+ { "prterase", FIELD_LFLAG, ECHOPRT, 0 },
+ { "-prterase", FIELD_LFLAG, 0, ECHOPRT },
+#endif
+ { "isig", FIELD_LFLAG, ISIG, 0 },
+ { "-isig", FIELD_LFLAG, 0, ISIG },
+ { "icanon", FIELD_LFLAG, ICANON, 0 },
+ { "-icanon", FIELD_LFLAG, 0, ICANON },
+ { "noflsh", FIELD_LFLAG, NOFLSH, 0 },
+ { "-noflsh", FIELD_LFLAG, 0, NOFLSH },
+ { "tostop", FIELD_LFLAG, TOSTOP, 0 },
+ { "-tostop", FIELD_LFLAG, 0, TOSTOP },
+#ifdef FLUSHO
+ { "flusho", FIELD_LFLAG, FLUSHO, 0 },
+ { "-flusho", FIELD_LFLAG, 0, FLUSHO },
+#endif
+#ifdef PENDIN
+ { "pendin", FIELD_LFLAG, PENDIN, 0 },
+ { "-pendin", FIELD_LFLAG, 0, PENDIN },
+#endif
+#ifdef EXTPROC
+ { "extproc", FIELD_LFLAG, EXTPROC, 0 },
+ { "-extproc", FIELD_LFLAG, 0, EXTPROC },
+#endif
+#if defined(ECHOKE) && defined(ECHOCTL) && defined(ECHOPRT)
+ { "crt", FIELD_LFLAG, ECHOE | ECHOKE | ECHOCTL, ECHOK | ECHOPRT },
+ { "-crt", FIELD_LFLAG, ECHOK, ECHOE | ECHOKE | ECHOCTL },
+ { "newcrt", FIELD_LFLAG, ECHOE | ECHOKE | ECHOCTL, ECHOK | ECHOPRT },
+ { "-newcrt", FIELD_LFLAG, ECHOK, ECHOE | ECHOKE | ECHOCTL },
+#endif
+
+ { "opost", FIELD_OFLAG, OPOST, 0 },
+ { "-opost", FIELD_OFLAG, 0, OPOST },
+ { "litout", FIELD_OFLAG, 0, OPOST },
+ { "-litout", FIELD_OFLAG, OPOST, 0 },
+ { "onlcr", FIELD_OFLAG, ONLCR, 0 },
+ { "-onlcr", FIELD_OFLAG, 0, ONLCR },
+ { "ocrnl", FIELD_OFLAG, OCRNL, 0 },
+ { "-ocrnl", FIELD_OFLAG, 0, OCRNL },
+#ifdef TABDLY
+#ifdef TAB0
+ { "tabs", FIELD_OFLAG, TAB0, TABDLY },
+#endif
+#ifdef TAB3
+ { "-tabs", FIELD_OFLAG, TAB3, TABDLY },
+ { "oxtabs", FIELD_OFLAG, TAB3, TABDLY },
+#endif
+#ifdef TAB0
+ { "-oxtabs", FIELD_OFLAG, TAB0, TABDLY },
+ { "tab0", FIELD_OFLAG, TAB0, TABDLY },
+#endif
+#ifdef TAB3
+ { "tab3", FIELD_OFLAG, TAB3, TABDLY },
+#endif
+#endif
+ { "onocr", FIELD_OFLAG, ONOCR, 0 },
+ { "-onocr", FIELD_OFLAG, 0, ONOCR },
+ { "onlret", FIELD_OFLAG, ONLRET, 0 },
+ { "-onlret", FIELD_OFLAG, 0, ONLRET },
+};
+
+static const struct print_flag local_print_flags[] = {
+ { "-icanon", FIELD_LFLAG, ICANON },
+ { "-isig", FIELD_LFLAG, ISIG },
+ { "-iexten", FIELD_LFLAG, IEXTEN },
+ { "-echo", FIELD_LFLAG, ECHO },
+ { "-echoe", FIELD_LFLAG, ECHOE },
+ { "-echok", FIELD_LFLAG, ECHOK },
+#ifdef ECHOKE
+ { "-echoke", FIELD_LFLAG, ECHOKE },
+#endif
+ { "-echonl", FIELD_LFLAG, ECHONL },
+#ifdef ECHOCTL
+ { "-echoctl", FIELD_LFLAG, ECHOCTL },
+#endif
+#ifdef ECHOPRT
+ { "-echoprt", FIELD_LFLAG, ECHOPRT },
+#endif
+ { "-noflsh", FIELD_LFLAG, NOFLSH },
+ { "-tostop", FIELD_LFLAG, TOSTOP },
+#ifdef FLUSHO
+ { "-flusho", FIELD_LFLAG, FLUSHO },
+#endif
+#ifdef PENDIN
+ { "-pendin", FIELD_LFLAG, PENDIN },
+#endif
+#ifdef EXTPROC
+ { "-extproc", FIELD_LFLAG, EXTPROC },
+#endif
+};
+
+static const struct print_flag input_print_flags[] = {
+ { "-istrip", FIELD_IFLAG, ISTRIP },
+ { "-icrnl", FIELD_IFLAG, ICRNL },
+ { "-inlcr", FIELD_IFLAG, INLCR },
+ { "-igncr", FIELD_IFLAG, IGNCR },
+ { "-ixon", FIELD_IFLAG, IXON },
+ { "-ixoff", FIELD_IFLAG, IXOFF },
+ { "-ixany", FIELD_IFLAG, IXANY },
+#ifdef IMAXBEL
+ { "-imaxbel", FIELD_IFLAG, IMAXBEL },
+#endif
+ { "-ignbrk", FIELD_IFLAG, IGNBRK },
+ { "-brkint", FIELD_IFLAG, BRKINT },
+ { "-inpck", FIELD_IFLAG, INPCK },
+ { "-ignpar", FIELD_IFLAG, IGNPAR },
+ { "-parmrk", FIELD_IFLAG, PARMRK },
+#ifdef IUTF8
+ { "-iutf8", FIELD_IFLAG, IUTF8 },
+#endif
+};
+
+static const struct print_flag output_print_flags[] = {
+ { "-opost", FIELD_OFLAG, OPOST },
+ { "-onlcr", FIELD_OFLAG, ONLCR },
+ { "-ocrnl", FIELD_OFLAG, OCRNL },
+ { "-onocr", FIELD_OFLAG, ONOCR },
+ { "-onlret", FIELD_OFLAG, ONLRET },
+};
+
+static const struct print_flag control_print_flags[] = {
+ { "-cread", FIELD_CFLAG, CREAD },
+ { "-parenb", FIELD_CFLAG, PARENB },
+ { "-parodd", FIELD_CFLAG, PARODD },
+ { "-hupcl", FIELD_CFLAG, HUPCL },
+ { "-clocal", FIELD_CFLAG, CLOCAL },
+ { "-cstopb", FIELD_CFLAG, CSTOPB },
+#ifdef CRTSCTS
+ { "-crtscts", FIELD_CFLAG, CRTSCTS },
+#endif
+};
+
+static const struct control_char control_chars[] = {
+#ifdef VDISCARD
+#ifdef CDISCARD
+ { "discard", VDISCARD, CDISCARD, false },
+#else
+ { "discard", VDISCARD, _POSIX_VDISABLE, false },
+#endif
+#endif
+#ifdef VEOF
+ { "eof", VEOF, CEOF, false },
+#endif
+#ifdef VEOL
+#ifdef CEOL
+ { "eol", VEOL, CEOL, false },
+#else
+ { "eol", VEOL, _POSIX_VDISABLE, false },
+#endif
+#endif
+#ifdef VEOL2
+#ifdef CEOL
+ { "eol2", VEOL2, CEOL, false },
+#else
+ { "eol2", VEOL2, _POSIX_VDISABLE, false },
+#endif
+#endif
+#ifdef VERASE
+ { "erase", VERASE, CERASE, false },
+#endif
+#ifdef VINTR
+ { "intr", VINTR, CINTR, false },
+#endif
+#ifdef VKILL
+ { "kill", VKILL, CKILL, false },
+#endif
+#ifdef VLNEXT
+#ifdef CLNEXT
+ { "lnext", VLNEXT, CLNEXT, false },
+#else
+ { "lnext", VLNEXT, _POSIX_VDISABLE, false },
+#endif
+#endif
+#ifdef VMIN
+ { "min", VMIN, 1, true },
+#endif
+#ifdef VQUIT
+ { "quit", VQUIT, CQUIT, false },
+#endif
+#ifdef VREPRINT
+#ifdef CREPRINT
+ { "reprint", VREPRINT, CREPRINT, false },
+#else
+ { "reprint", VREPRINT, _POSIX_VDISABLE, false },
+#endif
+#endif
+#ifdef VSTART
+ { "start", VSTART, CSTART, false },
+#endif
+#ifdef VSTOP
+ { "stop", VSTOP, CSTOP, false },
+#endif
+#ifdef VSUSP
+ { "susp", VSUSP, CSUSP, false },
+#endif
+#ifdef VTIME
+ { "time", VTIME, 0, true },
+#endif
+#ifdef VWERASE
+#ifdef CWERASE
+ { "werase", VWERASE, CWERASE, false },
+#else
+ { "werase", VWERASE, _POSIX_VDISABLE, false },
+#endif
+#endif
+};
+
+static const struct named_command commands[] = {
+ { "all", false, false, CMD_PRINT_BSD },
+ { "everything", false, false, CMD_PRINT_BSD },
+ { "cooked", false, false, CMD_SANE },
+ { "sane", false, false, CMD_SANE },
+ { "cbreak", true, false, CMD_CBREAK },
+ { "cols", false, true, CMD_COLUMNS },
+ { "columns", false, true, CMD_COLUMNS },
+ { "rows", false, true, CMD_ROWS },
+ { "size", false, false, CMD_SIZE },
+ { "speed", false, true, CMD_SPEED },
+ { "ispeed", false, true, CMD_ISPEED },
+ { "ospeed", false, true, CMD_OSPEED },
+ { "dec", false, false, CMD_DEC },
+ { "tty", false, false, CMD_TTY },
+ { "new", false, false, CMD_TTY },
+ { "old", false, false, CMD_TTY },
+ { "nl", true, false, CMD_NL },
+ { "raw", true, false, CMD_RAW },
+ { "ek", false, false, CMD_EK },
+};
+
+int
+main(int argc, char *argv[])
+{
+ enum output_format format;
+ const char *path;
+ int index;
+ bool format_set;
+
+ format = FORMAT_DEFAULT;
+ path = NULL;
+ index = 1;
+ format_set = false;
+
+ while (index < argc) {
+ const char *argument = argv[index];
+
+ if (strcmp(argument, "--") == 0) {
+ index++;
+ break;
+ }
+ if (strcmp(argument, "-f") == 0) {
+ index++;
+ if (index >= argc)
+ usage_error("option -f requires an argument");
+ path = argv[index++];
+ continue;
+ }
+ if (strncmp(argument, "-f", 2) == 0 && argument[2] != '\0') {
+ path = argument + 2;
+ index++;
+ continue;
+ }
+ if (argument[0] != '-' || argument[1] == '\0' ||
+ strspn(argument + 1, "aeg") != strlen(argument + 1))
+ break;
+
+ for (size_t offset = 1; argument[offset] != '\0'; offset++) {
+ enum output_format selected;
+
+ switch (argument[offset]) {
+ case 'a':
+ selected = FORMAT_POSIX;
+ break;
+ case 'e':
+ selected = FORMAT_BSD;
+ break;
+ case 'g':
+ selected = FORMAT_GFMT;
+ break;
+ default:
+ usage();
+ }
+ if (format_set && selected != format)
+ usage_error("options -a, -e, and -g are mutually exclusive");
+ format = selected;
+ format_set = true;
+ }
+ index++;
+ }
+
+ struct state state;
+ init_state(&state, path);
+
+ if (format == FORMAT_BSD || format == FORMAT_POSIX || format == FORMAT_GFMT ||
+ index >= argc)
+ print_state(&state, format);
+
+ while (index < argc)
+ apply_named_argument(&state, argv, argc, &index);
+
+ if (state.termios_dirty && tcsetattr(state.fd, TCSANOW, &state.termios) != 0)
+ die_errno("stty: tcsetattr failed for %s", state.label);
+ if (state.winsize_dirty &&
+ ioctl(state.fd, TIOCSWINSZ, &state.winsize) != 0)
+ die_errno("stty: TIOCSWINSZ failed for %s", state.label);
+ if (state.close_fd)
+ close(state.fd);
+ return 0;
+}
+
+static void
+init_state(struct state *state, const char *path)
+{
+ int fd;
+
+ memset(state, 0, sizeof(*state));
+
+ if (path == NULL) {
+ state->fd = STDIN_FILENO;
+ state->label = "stdin";
+ state->close_fd = false;
+ } else {
+ fd = open(path, O_RDWR | O_NOCTTY | O_NONBLOCK | O_CLOEXEC);
+ if (fd < 0 && (errno == EACCES || errno == EPERM))
+ fd = open(path, O_RDONLY | O_NOCTTY | O_NONBLOCK | O_CLOEXEC);
+ if (fd < 0)
+ die_errno("stty: cannot open %s", path);
+ state->fd = fd;
+ state->label = path;
+ state->close_fd = true;
+ }
+
+ if (tcgetattr(state->fd, &state->termios) != 0) {
+ if (errno == ENOTTY)
+ die("stty: %s is not a terminal", state->label);
+ die_errno("stty: tcgetattr failed for %s", state->label);
+ }
+ make_sane_termios(&state->sane, &state->termios);
+
+ memset(&state->winsize, 0, sizeof(state->winsize));
+ if (ioctl(state->fd, TIOCGWINSZ, &state->winsize) == 0)
+ state->have_winsize = true;
+
+#ifdef TIOCGETD
+ if (ioctl(state->fd, TIOCGETD, &state->line_discipline) == 0)
+ state->have_line_discipline = true;
+#endif
+}
+
+static void
+make_sane_termios(struct termios *sane, const struct termios *current)
+{
+ *sane = *current;
+
+ sane->c_iflag = BRKINT | ICRNL | IXON;
+#ifdef IMAXBEL
+ sane->c_iflag |= IMAXBEL;
+#endif
+#ifdef IUTF8
+ sane->c_iflag |= IUTF8;
+#endif
+
+ sane->c_oflag = OPOST | ONLCR;
+
+ sane->c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB);
+#ifdef CRTSCTS
+ sane->c_cflag &= ~CRTSCTS;
+#endif
+ sane->c_cflag |= CREAD | CS8;
+#ifdef HUPCL
+ sane->c_cflag |= HUPCL;
+#endif
+ sane->c_cflag |= current->c_cflag & CLOCAL;
+
+ sane->c_lflag = ISIG | ICANON | IEXTEN | ECHO | ECHOE | ECHOK;
+#ifdef ECHOKE
+ sane->c_lflag |= ECHOKE;
+#endif
+#ifdef ECHOCTL
+ sane->c_lflag |= ECHOCTL;
+#endif
+
+ memset(sane->c_cc, _POSIX_VDISABLE, sizeof(sane->c_cc));
+ for (size_t i = 0; i < ARRAY_LEN(control_chars); i++)
+ sane->c_cc[control_chars[i].index] = control_chars[i].default_value;
+
+ if (cfsetispeed(sane, cfgetispeed(current)) != 0 ||
+ cfsetospeed(sane, cfgetospeed(current)) != 0)
+ die_errno("stty: failed to preserve terminal speed");
+}
+
+static void
+apply_named_argument(struct state *state, char **argv, int argc, int *index)
+{
+ const char *argument = argv[*index];
+ const char *reason;
+
+ if ((reason = unsupported_reason(argument)) != NULL)
+ die_unsupported(argument, reason);
+ if (maybe_apply_gfmt(state, argument)) {
+ (*index)++;
+ return;
+ }
+ if (maybe_apply_flag_action(state, argument)) {
+ (*index)++;
+ return;
+ }
+ if (maybe_apply_command(state, argv, argc, index))
+ return;
+ if (maybe_apply_control_char(state, argv, argc, index))
+ return;
+ if (maybe_apply_speed_token(state, argument)) {
+ (*index)++;
+ return;
+ }
+ die("stty: illegal argument: %s", argument);
+}
+
+static bool
+maybe_apply_gfmt(struct state *state, const char *argument)
+{
+ char *cursor;
+ char *text;
+
+ if (strncmp(argument, "gfmt1:", 6) != 0)
+ return false;
+
+ text = strdup(argument + 6);
+ if (text == NULL)
+ die_errno("stty: strdup failed");
+
+ for (cursor = text; cursor != NULL;) {
+ char *token;
+ char *equals;
+ char *name;
+ char *value;
+
+ token = strsep(&cursor, ":");
+ if (token == NULL || token[0] == '\0')
+ break;
+ equals = strchr(token, '=');
+ if (equals == NULL) {
+ free(text);
+ die("stty: invalid gfmt1 token: %s", token);
+ }
+ *equals = '\0';
+ name = token;
+ value = equals + 1;
+
+ if (strcmp(name, "cflag") == 0) {
+ state->termios.c_cflag =
+ (tcflag_t)parse_unsigned(value, 16, UINT_MAX, "gfmt1 cflag");
+ continue;
+ }
+ if (strcmp(name, "iflag") == 0) {
+ state->termios.c_iflag =
+ (tcflag_t)parse_unsigned(value, 16, UINT_MAX, "gfmt1 iflag");
+ continue;
+ }
+ if (strcmp(name, "lflag") == 0) {
+ state->termios.c_lflag =
+ (tcflag_t)parse_unsigned(value, 16, UINT_MAX, "gfmt1 lflag");
+ continue;
+ }
+ if (strcmp(name, "oflag") == 0) {
+ state->termios.c_oflag =
+ (tcflag_t)parse_unsigned(value, 16, UINT_MAX, "gfmt1 oflag");
+ continue;
+ }
+ if (strcmp(name, "ispeed") == 0) {
+ set_speed(state, "ispeed", value, true, false);
+ continue;
+ }
+ if (strcmp(name, "ospeed") == 0) {
+ set_speed(state, "ospeed", value, false, true);
+ continue;
+ }
+
+ const struct control_char *control = find_control_char(name);
+ if (control == NULL) {
+ free(text);
+ die("stty: invalid gfmt1 token: %s", name);
+ }
+ state->termios.c_cc[control->index] =
+ (cc_t)parse_unsigned(value, 16, UCHAR_MAX, name);
+ }
+
+ free(text);
+ state->termios_dirty = true;
+ return true;
+}
+
+static bool
+maybe_apply_flag_action(struct state *state, const char *argument)
+{
+ for (size_t i = 0; i < ARRAY_LEN(flag_actions); i++) {
+ tcflag_t *field;
+
+ if (strcmp(flag_actions[i].name, argument) != 0)
+ continue;
+ field = field_ptr(&state->termios, flag_actions[i].field);
+ *field &= ~flag_actions[i].clear_mask;
+ *field |= flag_actions[i].set_mask;
+ state->termios_dirty = true;
+ return true;
+ }
+ return false;
+}
+
+static bool
+maybe_apply_command(struct state *state, char **argv, int argc, int *index)
+{
+ const struct named_command *command;
+ const char *argument = argv[*index];
+ const char *name = argument;
+ const char *value = NULL;
+ bool off = false;
+
+ if (argument[0] == '-' && argument[1] != '\0') {
+ name = argument + 1;
+ off = true;
+ }
+
+ command = find_command(name);
+ if (command == NULL)
+ return false;
+ if (off && !command->allow_off)
+ die("stty: illegal argument: %s", argument);
+ if (command->needs_argument) {
+ if (*index + 1 >= argc)
+ die("stty: %s requires an argument", name);
+ value = argv[*index + 1];
+ }
+
+ switch (command->id) {
+ case CMD_PRINT_BSD:
+ print_state(state, FORMAT_BSD);
+ break;
+ case CMD_SANE:
+ apply_sane(state);
+ break;
+ case CMD_CBREAK:
+ apply_cbreak(state, off);
+ break;
+ case CMD_COLUMNS:
+ set_rows_or_columns(&state->winsize.ws_col, name, value);
+ state->winsize_dirty = true;
+ break;
+ case CMD_ROWS:
+ set_rows_or_columns(&state->winsize.ws_row, name, value);
+ state->winsize_dirty = true;
+ break;
+ case CMD_SIZE:
+ if (!state->have_winsize && !state->winsize_dirty)
+ die("stty: terminal window size is unavailable on %s", state->label);
+ printf("%u %u\n", state->winsize.ws_row, state->winsize.ws_col);
+ break;
+ case CMD_SPEED:
+ set_speed(state, "speed", value, true, true);
+ break;
+ case CMD_ISPEED:
+ set_speed(state, "ispeed", value, true, false);
+ break;
+ case CMD_OSPEED:
+ set_speed(state, "ospeed", value, false, true);
+ break;
+ case CMD_DEC:
+ apply_dec(state);
+ break;
+ case CMD_TTY:
+ apply_tty(state);
+ break;
+ case CMD_NL:
+ apply_nl(state, off);
+ break;
+ case CMD_RAW:
+ apply_raw(state, off);
+ break;
+ case CMD_EK:
+ apply_ek(state);
+ break;
+ }
+
+ *index += command->needs_argument ? 2 : 1;
+ return true;
+}
+
+static bool
+maybe_apply_control_char(struct state *state, char **argv, int argc, int *index)
+{
+ const struct control_char *control;
+
+ control = find_control_char(argv[*index]);
+ if (control == NULL)
+ return false;
+ if (*index + 1 >= argc)
+ die("stty: %s requires an argument", control->name);
+
+ state->termios.c_cc[control->index] =
+ parse_control_char_value(control, argv[*index + 1]);
+ state->termios_dirty = true;
+ *index += 2;
+ return true;
+}
+
+static bool
+maybe_apply_speed_token(struct state *state, const char *argument)
+{
+ if (!is_decimal_number(argument))
+ return false;
+ set_speed(state, "speed", argument, true, true);
+ return true;
+}
+
+static void
+print_state(const struct state *state, enum output_format format)
+{
+ unsigned int ispeed;
+ unsigned int ospeed;
+
+ ispeed = speed_to_baud(cfgetispeed(&state->termios));
+ ospeed = speed_to_baud(cfgetospeed(&state->termios));
+
+ if (format == FORMAT_GFMT) {
+ printf("gfmt1:cflag=%lx:iflag=%lx:lflag=%lx:oflag=%lx:",
+ (unsigned long)state->termios.c_cflag,
+ (unsigned long)state->termios.c_iflag,
+ (unsigned long)state->termios.c_lflag,
+ (unsigned long)state->termios.c_oflag);
+ for (size_t i = 0; i < ARRAY_LEN(control_chars); i++) {
+ printf("%s=%x:", control_chars[i].name,
+ state->termios.c_cc[control_chars[i].index]);
+ }
+ printf("ispeed=%u:ospeed=%u\n", ispeed, ospeed);
+ return;
+ }
+
+ if (state->have_line_discipline && state->line_discipline != N_TTY)
+ printf("#%d disc; ", state->line_discipline);
+
+ if (ispeed != ospeed)
+ printf("ispeed %u baud; ospeed %u baud;", ispeed, ospeed);
+ else
+ printf("speed %u baud;", ispeed);
+ if ((format == FORMAT_BSD || format == FORMAT_POSIX) && state->have_winsize)
+ printf(" %u rows; %u columns;",
+ state->winsize.ws_row, state->winsize.ws_col);
+ printf("\n");
+
+ print_boolean_group(state, format, "lflags",
+ local_print_flags, ARRAY_LEN(local_print_flags));
+ print_boolean_group(state, format, "iflags",
+ input_print_flags, ARRAY_LEN(input_print_flags));
+ print_boolean_group(state, format, "oflags",
+ output_print_flags, ARRAY_LEN(output_print_flags));
+
+ struct line_builder builder;
+ line_builder_begin(&builder, "oflags");
+#ifdef TABDLY
+ {
+ tcflag_t current = state->termios.c_oflag & TABDLY;
+ tcflag_t sane = state->sane.c_oflag & TABDLY;
+
+ if (format != FORMAT_DEFAULT || current != sane) {
+#ifdef TAB3
+ if (current == TAB3)
+ line_builder_put(&builder, "tab3");
+ else
+#endif
+#ifdef TAB0
+ line_builder_put(&builder, "tab0");
+#else
+ line_builder_put(&builder, "tabs");
+#endif
+ }
+ }
+#endif
+ line_builder_finish(&builder);
+
+ print_boolean_group(state, format, "cflags",
+ control_print_flags, ARRAY_LEN(control_print_flags));
+
+ line_builder_begin(&builder, "cflags");
+ {
+ tcflag_t current = state->termios.c_cflag & CSIZE;
+ tcflag_t sane = state->sane.c_cflag & CSIZE;
+ if (format != FORMAT_DEFAULT || current != sane) {
+ switch (current) {
+ case CS5:
+ line_builder_put(&builder, "cs5");
+ break;
+ case CS6:
+ line_builder_put(&builder, "cs6");
+ break;
+ case CS7:
+ line_builder_put(&builder, "cs7");
+ break;
+ default:
+ line_builder_put(&builder, "cs8");
+ break;
+ }
+ }
+ }
+ line_builder_finish(&builder);
+
+ print_control_chars(state, format);
+}
+
+static void
+print_boolean_group(const struct state *state, enum output_format format,
+ const char *label, const struct print_flag *flags, size_t count)
+{
+ struct line_builder builder;
+
+ line_builder_begin(&builder, label);
+ for (size_t i = 0; i < count; i++) {
+ bool current;
+ bool sane;
+
+ current = (field_value(&state->termios, flags[i].field) & flags[i].mask) != 0;
+ sane = (field_value(&state->sane, flags[i].field) & flags[i].mask) != 0;
+ if (format == FORMAT_DEFAULT && current == sane)
+ continue;
+ line_builder_put(&builder, current ? flags[i].off_name + 1 : flags[i].off_name);
+ }
+ line_builder_finish(&builder);
+}
+
+static void
+print_control_chars(const struct state *state, enum output_format format)
+{
+ char name_line[128];
+ char value_line[128];
+ char buffer[16];
+ int count = 0;
+
+ name_line[0] = '\0';
+ value_line[0] = '\0';
+
+ if (format == FORMAT_POSIX) {
+ struct line_builder builder;
+
+ line_builder_begin(&builder, "cchars");
+ for (size_t i = 0; i < ARRAY_LEN(control_chars); i++) {
+ char token[32];
+ const struct control_char *control = &control_chars[i];
+
+ snprintf(token, sizeof(token), "%s = %s;",
+ control->name,
+ format_control_char(control,
+ state->termios.c_cc[control->index],
+ buffer, sizeof(buffer)));
+ line_builder_put(&builder, token);
+ }
+ line_builder_finish(&builder);
+ return;
+ }
+
+ for (size_t i = 0; i < ARRAY_LEN(control_chars); i++) {
+ const struct control_char *control = &control_chars[i];
+ cc_t current = state->termios.c_cc[control->index];
+ cc_t sane = state->sane.c_cc[control->index];
+ const char *formatted;
+
+ if (format == FORMAT_DEFAULT && current == sane)
+ continue;
+
+ formatted = format_control_char(control, current, buffer, sizeof(buffer));
+ snprintf(name_line + count * 8, sizeof(name_line) - (size_t)count * 8,
+ "%-8s", control->name);
+ snprintf(value_line + count * 8, sizeof(value_line) - (size_t)count * 8,
+ "%-8s", formatted);
+ count++;
+ if (count == 8) {
+ printf("%s\n%s\n", name_line, value_line);
+ count = 0;
+ name_line[0] = '\0';
+ value_line[0] = '\0';
+ }
+ }
+ if (count != 0)
+ printf("%s\n%s\n", name_line, value_line);
+}
+
+static void
+line_builder_begin(struct line_builder *builder, const char *label)
+{
+ builder->label = label;
+ builder->column = 0;
+}
+
+static void
+line_builder_put(struct line_builder *builder, const char *token)
+{
+ size_t length = strlen(token);
+
+ if (builder->column == 0) {
+ if (builder->label != NULL)
+ builder->column = (size_t)printf("%s: %s", builder->label, token);
+ else
+ builder->column = (size_t)printf("%s", token);
+ return;
+ }
+ if (builder->column + 1 + length > OUTPUT_WIDTH) {
+ printf("\n\t%s", token);
+ builder->column = 8 + length;
+ return;
+ }
+ printf(" %s", token);
+ builder->column += 1 + length;
+}
+
+static void
+line_builder_finish(struct line_builder *builder)
+{
+ if (builder->column != 0)
+ printf("\n");
+ builder->column = 0;
+}
+
+static tcflag_t *
+field_ptr(struct termios *termios, enum termios_field field)
+{
+ switch (field) {
+ case FIELD_IFLAG:
+ return &termios->c_iflag;
+ case FIELD_OFLAG:
+ return &termios->c_oflag;
+ case FIELD_CFLAG:
+ return &termios->c_cflag;
+ case FIELD_LFLAG:
+ return &termios->c_lflag;
+ }
+ die("stty: internal error: unknown termios field");
+ return &termios->c_iflag;
+}
+
+static tcflag_t
+field_value(const struct termios *termios, enum termios_field field)
+{
+ switch (field) {
+ case FIELD_IFLAG:
+ return termios->c_iflag;
+ case FIELD_OFLAG:
+ return termios->c_oflag;
+ case FIELD_CFLAG:
+ return termios->c_cflag;
+ case FIELD_LFLAG:
+ return termios->c_lflag;
+ }
+ die("stty: internal error: unknown termios field");
+ return 0;
+}
+
+static const struct control_char *
+find_control_char(const char *name)
+{
+ if (strcmp(name, "brk") == 0)
+ name = "eol";
+ else if (strcmp(name, "flush") == 0)
+ name = "discard";
+ else if (strcmp(name, "rprnt") == 0)
+ name = "reprint";
+
+ for (size_t i = 0; i < ARRAY_LEN(control_chars); i++) {
+ if (strcmp(control_chars[i].name, name) == 0)
+ return &control_chars[i];
+ }
+ return NULL;
+}
+
+static const struct named_command *
+find_command(const char *name)
+{
+ for (size_t i = 0; i < ARRAY_LEN(commands); i++) {
+ if (strcmp(commands[i].name, name) == 0)
+ return &commands[i];
+ }
+ return NULL;
+}
+
+static const char *
+unsupported_reason(const char *name)
+{
+ if (strcmp(name, "altwerase") == 0 || strcmp(name, "-altwerase") == 0)
+ return "Linux termios has no ALTWERASE local mode";
+ if (strcmp(name, "mdmbuf") == 0 || strcmp(name, "-mdmbuf") == 0)
+ return "Linux termios has no carrier-detect output flow-control flag";
+ if (strcmp(name, "rtsdtr") == 0 || strcmp(name, "-rtsdtr") == 0)
+ return "Linux termios cannot control BSD RTS/DTR-on-open semantics";
+ if (strcmp(name, "kerninfo") == 0 || strcmp(name, "-kerninfo") == 0 ||
+ strcmp(name, "nokerninfo") == 0 || strcmp(name, "-nokerninfo") == 0)
+ return "Linux has no STATUS control character or kernel tty status line";
+ if (strcmp(name, "status") == 0)
+ return "Linux termios has no VSTATUS control character";
+ if (strcmp(name, "dsusp") == 0)
+ return "Linux termios has no VDSUSP control character";
+ if (strcmp(name, "erase2") == 0)
+ return "Linux termios has no VERASE2 control character";
+ return NULL;
+}
+
+static void
+set_rows_or_columns(unsigned short *slot, const char *keyword, const char *value)
+{
+ *slot = (unsigned short)parse_unsigned(value, 10, USHRT_MAX, keyword);
+}
+
+static void
+set_speed(struct state *state, const char *keyword, const char *value, bool set_input,
+ bool set_output)
+{
+ bool found;
+ speed_t symbol;
+ unsigned int baud;
+
+ baud = (unsigned int)parse_unsigned(value, 10, UINT_MAX, keyword);
+ symbol = lookup_speed_symbol(baud, &found);
+ if (!found)
+ die("stty: unsupported baud rate on Linux termios API: %u", baud);
+ if (set_input && cfsetispeed(&state->termios, symbol) != 0)
+ die_errno("stty: cannot set input speed to %u", baud);
+ if (set_output && cfsetospeed(&state->termios, symbol) != 0)
+ die_errno("stty: cannot set output speed to %u", baud);
+ state->termios_dirty = true;
+}
+
+static speed_t
+lookup_speed_symbol(unsigned int baud, bool *found)
+{
+ for (size_t i = 0; i < ARRAY_LEN(speed_table); i++) {
+ if (speed_table[i].baud == baud) {
+ *found = true;
+ return speed_table[i].symbol;
+ }
+ }
+ *found = false;
+ return (speed_t)0;
+}
+
+static unsigned int
+speed_to_baud(speed_t symbol)
+{
+ for (size_t i = 0; i < ARRAY_LEN(speed_table); i++) {
+ if (speed_table[i].symbol == symbol)
+ return speed_table[i].baud;
+ }
+ return (unsigned int)symbol;
+}
+
+static unsigned long long
+parse_unsigned(const char *text, int base, unsigned long long max_value, const char *what)
+{
+ char *end;
+ unsigned long long value;
+
+ errno = 0;
+ value = strtoull(text, &end, base);
+ if (errno == ERANGE || value > max_value)
+ die("stty: %s requires a number between 0 and %llu", what, max_value);
+ if (text[0] == '\0' || end == text || *end != '\0')
+ die("stty: %s requires a number between 0 and %llu", what, max_value);
+ return value;
+}
+
+static bool
+is_decimal_number(const char *text)
+{
+ if (*text == '\0')
+ return false;
+ for (const unsigned char *p = (const unsigned char *)text; *p != '\0'; p++) {
+ if (!isdigit(*p))
+ return false;
+ }
+ return true;
+}
+
+static cc_t
+parse_control_char_value(const struct control_char *control, const char *value)
+{
+ if (strcmp(value, "undef") == 0 || strcmp(value, "<undef>") == 0 ||
+ strcmp(value, "^-") == 0)
+ return _POSIX_VDISABLE;
+ if (control->numeric)
+ return (cc_t)parse_unsigned(value, 10, UCHAR_MAX, control->name);
+ if (value[0] == '^' && value[1] != '\0' && value[2] == '\0') {
+ if (value[1] == '?')
+ return 0177;
+ if (value[1] == '-')
+ return _POSIX_VDISABLE;
+ return (cc_t)(value[1] & 037);
+ }
+ if (value[0] != '\0' && value[1] == '\0')
+ return (cc_t)value[0];
+ die("stty: control character %s requires a single character, ^X, ^?, ^-, or undef",
+ control->name);
+ return 0;
+}
+
+static const char *
+format_control_char(const struct control_char *control, cc_t value, char *buffer,
+ size_t buffer_size)
+{
+ if (control->numeric) {
+ snprintf(buffer, buffer_size, "%u", (unsigned int)value);
+ return buffer;
+ }
+ if (value == _POSIX_VDISABLE)
+ return "<undef>";
+ if (value == 0177)
+ return "^?";
+ if (value < 32) {
+ buffer[0] = '^';
+ buffer[1] = (char)(value + '@');
+ buffer[2] = '\0';
+ return buffer;
+ }
+ if (isprint(value)) {
+ buffer[0] = (char)value;
+ buffer[1] = '\0';
+ return buffer;
+ }
+ snprintf(buffer, buffer_size, "0x%02x", value);
+ return buffer;
+}
+
+static void
+apply_sane(struct state *state)
+{
+ state->termios = state->sane;
+ state->termios_dirty = true;
+}
+
+static void
+apply_cbreak(struct state *state, bool off)
+{
+ if (off) {
+ apply_sane(state);
+ return;
+ }
+ state->termios.c_iflag |= BRKINT | IXON;
+#ifdef IMAXBEL
+ state->termios.c_iflag |= IMAXBEL;
+#endif
+ state->termios.c_oflag |= OPOST;
+ state->termios.c_lflag |= ISIG | IEXTEN;
+ state->termios.c_lflag &= ~ICANON;
+ state->termios_dirty = true;
+}
+
+static void
+apply_dec(struct state *state)
+{
+#ifdef VERASE
+ state->termios.c_cc[VERASE] = 0177;
+#endif
+#ifdef VKILL
+ state->termios.c_cc[VKILL] = ('u' & 037);
+#endif
+#ifdef VINTR
+ state->termios.c_cc[VINTR] = ('c' & 037);
+#endif
+ state->termios.c_iflag &= ~IXANY;
+ state->termios.c_lflag |= ECHOE;
+#ifdef ECHOKE
+ state->termios.c_lflag |= ECHOKE;
+#endif
+#ifdef ECHOCTL
+ state->termios.c_lflag |= ECHOCTL;
+#endif
+ state->termios.c_lflag &= ~ECHOK;
+#ifdef ECHOPRT
+ state->termios.c_lflag &= ~ECHOPRT;
+#endif
+ state->termios_dirty = true;
+}
+
+static void
+apply_nl(struct state *state, bool off)
+{
+ if (off) {
+ state->termios.c_iflag &= ~(ICRNL | INLCR | IGNCR);
+ state->termios.c_oflag &= ~ONLCR;
+ } else {
+ state->termios.c_iflag |= ICRNL;
+ state->termios.c_oflag |= ONLCR;
+ }
+ state->termios_dirty = true;
+}
+
+static void
+apply_raw(struct state *state, bool off)
+{
+ if (off) {
+ apply_sane(state);
+ return;
+ }
+ cfmakeraw(&state->termios);
+ state->termios.c_cflag &= ~(CSIZE | PARENB);
+ state->termios.c_cflag |= CS8;
+ state->termios_dirty = true;
+}
+
+static void
+apply_tty(struct state *state)
+{
+#ifdef TIOCSETD
+ int discipline = N_TTY;
+
+ if (ioctl(state->fd, TIOCSETD, &discipline) != 0)
+ die_errno("stty: TIOCSETD failed for %s", state->label);
+ state->line_discipline = discipline;
+ state->have_line_discipline = true;
+#else
+ (void)state;
+ die_unsupported("tty", "Linux headers do not expose TIOCSETD");
+#endif
+}
+
+static void
+apply_ek(struct state *state)
+{
+#ifdef VERASE2
+ state->termios.c_cc[VERASE2] = CERASE2;
+#else
+ die_unsupported("ek",
+ "Linux termios has no VERASE2 control character, so BSD ek semantics are incomplete");
+#endif
+#ifdef VERASE
+ state->termios.c_cc[VERASE] = CERASE;
+#endif
+#ifdef VKILL
+ state->termios.c_cc[VKILL] = CKILL;
+#endif
+ state->termios_dirty = true;
+}
+
+static noreturn void
+usage(void)
+{
+ fprintf(stderr, "usage: stty [-a | -e | -g] [-f file] [arguments]\n");
+ exit(1);
+}
+
+static noreturn void
+usage_error(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ fputs("stty: ", stderr);
+ vfprintf(stderr, fmt, ap);
+ fputc('\n', stderr);
+ va_end(ap);
+ usage();
+}
+
+static noreturn void
+die(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ fputc('\n', stderr);
+ va_end(ap);
+ exit(1);
+}
+
+static noreturn void
+die_errno(const char *fmt, ...)
+{
+ va_list ap;
+ int saved_errno = errno;
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fprintf(stderr, ": %s\n", strerror(saved_errno));
+ exit(1);
+}
+
+static noreturn void
+die_unsupported(const char *name, const char *reason)
+{
+ die("stty: argument '%s' is not supported on Linux: %s", name, reason);
+}
diff --git a/corebinutils/stty/tests/mkpty.c b/corebinutils/stty/tests/mkpty.c
new file mode 100644
index 0000000000..258fee18d4
--- /dev/null
+++ b/corebinutils/stty/tests/mkpty.c
@@ -0,0 +1,56 @@
+#define _XOPEN_SOURCE 700
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+int
+main(void)
+{
+ char buffer[256];
+ char *path;
+ int master;
+ ssize_t bytes;
+
+ master = posix_openpt(O_RDWR | O_NOCTTY | O_CLOEXEC);
+ if (master < 0) {
+ if (errno == ENOENT || errno == ENODEV || errno == ENOSYS ||
+ errno == EPERM || errno == EACCES)
+ return 77;
+ fprintf(stderr, "mkpty: posix_openpt: %s\n", strerror(errno));
+ return 1;
+ }
+ if (grantpt(master) != 0) {
+ fprintf(stderr, "mkpty: grantpt: %s\n", strerror(errno));
+ close(master);
+ return 1;
+ }
+ if (unlockpt(master) != 0) {
+ fprintf(stderr, "mkpty: unlockpt: %s\n", strerror(errno));
+ close(master);
+ return 1;
+ }
+ path = ptsname(master);
+ if (path == NULL) {
+ fprintf(stderr, "mkpty: ptsname: %s\n", strerror(errno));
+ close(master);
+ return 1;
+ }
+ if (printf("%s\n", path) < 0 || fflush(stdout) != 0) {
+ fprintf(stderr, "mkpty: failed to write slave path\n");
+ close(master);
+ return 1;
+ }
+ while ((bytes = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0)
+ ;
+ if (bytes < 0) {
+ fprintf(stderr, "mkpty: read: %s\n", strerror(errno));
+ close(master);
+ return 1;
+ }
+ close(master);
+ return 0;
+}
diff --git a/corebinutils/stty/tests/termios_probe.c b/corebinutils/stty/tests/termios_probe.c
new file mode 100644
index 0000000000..6478aa4358
--- /dev/null
+++ b/corebinutils/stty/tests/termios_probe.c
@@ -0,0 +1,226 @@
+#define _DEFAULT_SOURCE 1
+#define _XOPEN_SOURCE 700
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#ifndef N_TTY
+#define N_TTY 0
+#endif
+
+struct speed_map {
+ unsigned int baud;
+ speed_t symbol;
+};
+
+static const struct speed_map speed_table[] = {
+#ifdef B0
+ { 0U, B0 },
+#endif
+#ifdef B50
+ { 50U, B50 },
+#endif
+#ifdef B75
+ { 75U, B75 },
+#endif
+#ifdef B110
+ { 110U, B110 },
+#endif
+#ifdef B134
+ { 134U, B134 },
+#endif
+#ifdef B150
+ { 150U, B150 },
+#endif
+#ifdef B200
+ { 200U, B200 },
+#endif
+#ifdef B300
+ { 300U, B300 },
+#endif
+#ifdef B600
+ { 600U, B600 },
+#endif
+#ifdef B1200
+ { 1200U, B1200 },
+#endif
+#ifdef B1800
+ { 1800U, B1800 },
+#endif
+#ifdef B2400
+ { 2400U, B2400 },
+#endif
+#ifdef B4800
+ { 4800U, B4800 },
+#endif
+#ifdef B9600
+ { 9600U, B9600 },
+#endif
+#ifdef B19200
+ { 19200U, B19200 },
+#endif
+#ifdef B38400
+ { 38400U, B38400 },
+#endif
+#ifdef B57600
+ { 57600U, B57600 },
+#endif
+#ifdef B115200
+ { 115200U, B115200 },
+#endif
+#ifdef B230400
+ { 230400U, B230400 },
+#endif
+#ifdef B460800
+ { 460800U, B460800 },
+#endif
+#ifdef B500000
+ { 500000U, B500000 },
+#endif
+#ifdef B576000
+ { 576000U, B576000 },
+#endif
+#ifdef B921600
+ { 921600U, B921600 },
+#endif
+#ifdef B1000000
+ { 1000000U, B1000000 },
+#endif
+#ifdef B1152000
+ { 1152000U, B1152000 },
+#endif
+#ifdef B1500000
+ { 1500000U, B1500000 },
+#endif
+#ifdef B2000000
+ { 2000000U, B2000000 },
+#endif
+#ifdef B2500000
+ { 2500000U, B2500000 },
+#endif
+#ifdef B3000000
+ { 3000000U, B3000000 },
+#endif
+#ifdef B3500000
+ { 3500000U, B3500000 },
+#endif
+#ifdef B4000000
+ { 4000000U, B4000000 },
+#endif
+};
+
+static unsigned int
+speed_to_baud(speed_t symbol)
+{
+ for (size_t i = 0; i < sizeof(speed_table) / sizeof(speed_table[0]); i++) {
+ if (speed_table[i].symbol == symbol)
+ return speed_table[i].baud;
+ }
+ return (unsigned int)symbol;
+}
+
+static void
+print_cc(const char *name, const struct termios *termios, size_t index)
+{
+ printf("%s=%u\n", name, (unsigned int)termios->c_cc[index]);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct termios termios;
+ struct winsize winsize;
+ int fd;
+ int line_discipline;
+
+ if (argc != 2) {
+ fprintf(stderr, "usage: %s path\n", argv[0]);
+ return 1;
+ }
+
+ fd = open(argv[1], O_RDONLY | O_NOCTTY | O_NONBLOCK | O_CLOEXEC);
+ if (fd < 0) {
+ fprintf(stderr, "termios_probe: open: %s\n", strerror(errno));
+ return 1;
+ }
+ if (tcgetattr(fd, &termios) != 0) {
+ fprintf(stderr, "termios_probe: tcgetattr: %s\n", strerror(errno));
+ close(fd);
+ return 1;
+ }
+ memset(&winsize, 0, sizeof(winsize));
+ if (ioctl(fd, TIOCGWINSZ, &winsize) != 0) {
+ fprintf(stderr, "termios_probe: TIOCGWINSZ: %s\n", strerror(errno));
+ close(fd);
+ return 1;
+ }
+
+ printf("ispeed=%u\n", speed_to_baud(cfgetispeed(&termios)));
+ printf("ospeed=%u\n", speed_to_baud(cfgetospeed(&termios)));
+ printf("row=%u\n", winsize.ws_row);
+ printf("col=%u\n", winsize.ws_col);
+
+#ifdef TIOCGETD
+ if (ioctl(fd, TIOCGETD, &line_discipline) == 0)
+ printf("ldisc=%d\n", line_discipline);
+#else
+ printf("ldisc=%d\n", N_TTY);
+#endif
+
+ printf("echo=%d\n", (termios.c_lflag & ECHO) != 0);
+ printf("icanon=%d\n", (termios.c_lflag & ICANON) != 0);
+ printf("isig=%d\n", (termios.c_lflag & ISIG) != 0);
+ printf("iexten=%d\n", (termios.c_lflag & IEXTEN) != 0);
+#ifdef EXTPROC
+ printf("extproc=%d\n", (termios.c_lflag & EXTPROC) != 0);
+#endif
+ printf("opost=%d\n", (termios.c_oflag & OPOST) != 0);
+ printf("onlcr=%d\n", (termios.c_oflag & ONLCR) != 0);
+ printf("ixany=%d\n", (termios.c_iflag & IXANY) != 0);
+ printf("ixon=%d\n", (termios.c_iflag & IXON) != 0);
+ printf("ixoff=%d\n", (termios.c_iflag & IXOFF) != 0);
+ printf("icrnl=%d\n", (termios.c_iflag & ICRNL) != 0);
+ printf("inlcr=%d\n", (termios.c_iflag & INLCR) != 0);
+ printf("igncr=%d\n", (termios.c_iflag & IGNCR) != 0);
+ printf("parenb=%d\n", (termios.c_cflag & PARENB) != 0);
+ printf("parodd=%d\n", (termios.c_cflag & PARODD) != 0);
+#ifdef CRTSCTS
+ printf("crtscts=%d\n", (termios.c_cflag & CRTSCTS) != 0);
+#endif
+
+ switch (termios.c_cflag & CSIZE) {
+ case CS5:
+ printf("csize=5\n");
+ break;
+ case CS6:
+ printf("csize=6\n");
+ break;
+ case CS7:
+ printf("csize=7\n");
+ break;
+ default:
+ printf("csize=8\n");
+ break;
+ }
+
+#ifdef VINTR
+ print_cc("intr", &termios, VINTR);
+#endif
+#ifdef VMIN
+ print_cc("min", &termios, VMIN);
+#endif
+#ifdef VTIME
+ print_cc("time", &termios, VTIME);
+#endif
+
+ close(fd);
+ return 0;
+}
diff --git a/corebinutils/stty/tests/test.sh b/corebinutils/stty/tests/test.sh
new file mode 100644
index 0000000000..5be7ea927d
--- /dev/null
+++ b/corebinutils/stty/tests/test.sh
@@ -0,0 +1,343 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+STTY_BIN=${STTY_BIN:-"$ROOT/out/stty"}
+CC=${CC:-cc}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/stty-test.XXXXXX")
+MKPTY_BIN="$WORKDIR/mkpty"
+PROBE_BIN="$WORKDIR/termios_probe"
+SLAVE_PATH_FILE="$WORKDIR/slave-path"
+HOLD_FIFO="$WORKDIR/hold"
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+PROBE_FILE="$WORKDIR/probe"
+MKPTY_PID=""
+HOLD_OPEN=0
+
+export LC_ALL=C
+
+cleanup() {
+ if [ "$HOLD_OPEN" -eq 1 ]; then
+ exec 3>&-
+ fi
+ if [ -n "$MKPTY_PID" ]; then
+ wait "$MKPTY_PID" 2>/dev/null || true
+ fi
+ rm -rf "$WORKDIR"
+}
+
+trap cleanup EXIT INT TERM
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected ---" >&2
+ printf '%s\n' "$expected" >&2
+ printf '%s\n' "--- actual ---" >&2
+ printf '%s\n' "$actual" >&2
+ exit 1
+ fi
+}
+
+assert_contains() {
+ name=$1
+ text=$2
+ pattern=$3
+ case $text in
+ *"$pattern"*) ;;
+ *) fail "$name" ;;
+ esac
+}
+
+assert_status() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" -ne "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "expected status: $expected" >&2
+ printf '%s\n' "actual status: $actual" >&2
+ exit 1
+ fi
+}
+
+run_capture() {
+ if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ LAST_STATUS=0
+ else
+ LAST_STATUS=$?
+ fi
+ LAST_STDOUT=$(cat "$STDOUT_FILE")
+ LAST_STDERR=$(cat "$STDERR_FILE")
+}
+
+build_helpers() {
+ "$CC" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -O2 -std=c17 -Wall -Wextra -Werror \
+ "$ROOT/tests/mkpty.c" -o "$MKPTY_BIN" \
+ || fail "failed to build mkpty helper"
+ "$CC" -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=700 -O2 -std=c17 -Wall -Wextra -Werror \
+ "$ROOT/tests/termios_probe.c" -o "$PROBE_BIN" \
+ || fail "failed to build termios_probe helper"
+}
+
+start_pty() {
+ mkfifo "$HOLD_FIFO"
+ "$MKPTY_BIN" <"$HOLD_FIFO" >"$SLAVE_PATH_FILE" 2>"$WORKDIR/mkpty.err" &
+ MKPTY_PID=$!
+ exec 3>"$HOLD_FIFO"
+ HOLD_OPEN=1
+
+ i=0
+ while [ ! -s "$SLAVE_PATH_FILE" ]; do
+ if ! kill -0 "$MKPTY_PID" 2>/dev/null; then
+ wait "$MKPTY_PID" || status=$?
+ status=${status:-0}
+ if [ "$status" -eq 77 ]; then
+ return 77
+ fi
+ fail "mkpty helper failed: $(cat "$WORKDIR/mkpty.err")"
+ fi
+ i=$((i + 1))
+ if [ "$i" -gt 100 ]; then
+ fail "mkpty helper timed out"
+ fi
+ sleep 0.05
+ done
+
+ SLAVE_PATH=$(cat "$SLAVE_PATH_FILE")
+ [ -n "$SLAVE_PATH" ] || fail "mkpty helper did not print a slave path"
+}
+
+probe_capture() {
+ "$PROBE_BIN" "$SLAVE_PATH" >"$PROBE_FILE" 2>"$WORKDIR/probe.err" \
+ || fail "termios probe failed: $(cat "$WORKDIR/probe.err")"
+ LAST_PROBE=$(cat "$PROBE_FILE")
+}
+
+probe_value() {
+ key=$1
+ value=$(sed -n "s/^$key=//p" "$PROBE_FILE")
+ [ -n "$value" ] || fail "missing probe key: $key"
+ printf '%s' "$value"
+}
+
+[ -x "$STTY_BIN" ] || fail "missing binary: $STTY_BIN"
+
+# In some harnesses (PTY-backed runners), stdin is an interactive tty.
+# This suite expects a detached stdin for its initial non-tty checks.
+if [ -t 0 ]; then
+ printf '%s\n' "SKIP: stty tests require non-interactive stdin in this environment" >&2
+ printf '%s\n' "PASS"
+ exit 0
+fi
+
+run_capture "$STTY_BIN"
+assert_status "stdin terminal check" 1 "$LAST_STATUS"
+assert_eq "stdin terminal stderr" "stty: stdin is not a terminal" "$LAST_STDERR"
+assert_eq "stdin terminal stdout" "" "$LAST_STDOUT"
+
+run_capture "$STTY_BIN" -a -g
+assert_status "mutually exclusive format options" 1 "$LAST_STATUS"
+assert_contains "mutually exclusive usage" "$LAST_STDERR" \
+ "usage: stty [-a | -e | -g] [-f file] [arguments]"
+
+build_helpers
+if ! start_pty; then
+ printf '%s\n' "SKIP: PTY-dependent tests are unavailable in this environment" >&2
+ printf '%s\n' "PASS"
+ exit 0
+fi
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH"
+assert_status "default output status" 0 "$LAST_STATUS"
+assert_contains "default output speed" "$LAST_STDOUT" "speed "
+
+run_capture "$STTY_BIN" -a -f "$SLAVE_PATH"
+assert_status "posix output status" 0 "$LAST_STATUS"
+assert_contains "posix output erase" "$LAST_STDOUT" "erase = "
+assert_contains "posix output min" "$LAST_STDOUT" "min = "
+
+run_capture "$STTY_BIN" -e -f "$SLAVE_PATH"
+assert_status "bsd output status" 0 "$LAST_STATUS"
+assert_contains "bsd output lflags" "$LAST_STDOUT" "lflags:"
+assert_contains "bsd output iflags" "$LAST_STDOUT" "iflags:"
+
+run_capture "$STTY_BIN" -g -f "$SLAVE_PATH"
+assert_status "gfmt output status" 0 "$LAST_STATUS"
+assert_contains "gfmt output prefix" "$LAST_STDOUT" "gfmt1:"
+SAVED_STATE=$LAST_STDOUT
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" rows 40 columns 120
+assert_status "rows columns status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "rows after set" "40" "$(probe_value row)"
+assert_eq "columns after set" "120" "$(probe_value col)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" size
+assert_status "size status" 0 "$LAST_STATUS"
+assert_eq "size output" "40 120" "$LAST_STDOUT"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" speed 9600
+assert_status "speed command status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "speed command ispeed" "9600" "$(probe_value ispeed)"
+assert_eq "speed command ospeed" "9600" "$(probe_value ospeed)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" ospeed 19200
+assert_status "ospeed command status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "ospeed command ispeed unchanged" "9600" "$(probe_value ispeed)"
+assert_eq "ospeed command ospeed" "19200" "$(probe_value ospeed)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" ispeed 4800
+assert_status "ispeed command status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "ispeed command ispeed" "4800" "$(probe_value ispeed)"
+assert_eq "ispeed command ospeed unchanged" "19200" "$(probe_value ospeed)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" 38400
+assert_status "bare speed status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "bare speed ispeed" "38400" "$(probe_value ispeed)"
+assert_eq "bare speed ospeed" "38400" "$(probe_value ospeed)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" intr '^A' min 7 time 9 -echo -icanon
+assert_status "control char and flag status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "intr control char" "1" "$(probe_value intr)"
+assert_eq "min control char" "7" "$(probe_value min)"
+assert_eq "time control char" "9" "$(probe_value time)"
+assert_eq "echo disabled" "0" "$(probe_value echo)"
+assert_eq "icanon disabled" "0" "$(probe_value icanon)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" raw
+assert_status "raw status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "raw echo" "0" "$(probe_value echo)"
+assert_eq "raw icanon" "0" "$(probe_value icanon)"
+assert_eq "raw isig" "0" "$(probe_value isig)"
+assert_eq "raw iexten" "0" "$(probe_value iexten)"
+assert_eq "raw opost" "0" "$(probe_value opost)"
+assert_eq "raw csize" "8" "$(probe_value csize)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" -raw
+assert_status "negative raw status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "sane echo" "1" "$(probe_value echo)"
+assert_eq "sane icanon" "1" "$(probe_value icanon)"
+assert_eq "sane isig" "1" "$(probe_value isig)"
+assert_eq "sane iexten" "1" "$(probe_value iexten)"
+assert_eq "sane opost" "1" "$(probe_value opost)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" extproc
+assert_status "extproc enable status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "extproc enabled" "1" "$(probe_value extproc)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" -extproc
+assert_status "extproc disable status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "extproc disabled" "0" "$(probe_value extproc)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" dec
+assert_status "dec status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "dec intr" "3" "$(probe_value intr)"
+assert_eq "dec ixany" "0" "$(probe_value ixany)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" cbreak
+assert_status "cbreak status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "cbreak icanon" "0" "$(probe_value icanon)"
+assert_eq "cbreak isig" "1" "$(probe_value isig)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" cooked
+assert_status "cooked status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "cooked echo" "1" "$(probe_value echo)"
+assert_eq "cooked icanon" "1" "$(probe_value icanon)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" tty
+assert_status "tty status" 0 "$LAST_STATUS"
+probe_capture
+assert_eq "tty line discipline" "0" "$(probe_value ldisc)"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" all
+assert_status "all compatibility status" 0 "$LAST_STATUS"
+assert_contains "all compatibility output" "$LAST_STDOUT" "lflags:"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" raw -echo min 5 time 8
+assert_status "prepare for gfmt restore" 0 "$LAST_STATUS"
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" "$SAVED_STATE"
+assert_status "gfmt restore status" 0 "$LAST_STATUS"
+run_capture "$STTY_BIN" -g -f "$SLAVE_PATH"
+assert_status "gfmt restore verify status" 0 "$LAST_STATUS"
+assert_eq "gfmt round trip" "$SAVED_STATE" "$LAST_STDOUT"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" rows nope
+assert_status "invalid rows status" 1 "$LAST_STATUS"
+assert_eq "invalid rows stderr" "stty: rows requires a number between 0 and 65535" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" min 999
+assert_status "invalid min status" 1 "$LAST_STATUS"
+assert_eq "invalid min stderr" "stty: min requires a number between 0 and 255" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" speed 12345
+assert_status "unsupported speed status" 1 "$LAST_STATUS"
+assert_eq "unsupported speed stderr" \
+ "stty: unsupported baud rate on Linux termios API: 12345" "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" speed
+assert_status "missing speed argument status" 1 "$LAST_STATUS"
+assert_eq "missing speed argument stderr" "stty: speed requires an argument" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" intr ab
+assert_status "invalid control char status" 1 "$LAST_STATUS"
+assert_eq "invalid control char stderr" \
+ "stty: control character intr requires a single character, ^X, ^?, ^-, or undef" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" erase2 x
+assert_status "unsupported erase2 status" 1 "$LAST_STATUS"
+assert_eq "unsupported erase2 stderr" \
+ "stty: argument 'erase2' is not supported on Linux: Linux termios has no VERASE2 control character" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" status '^T'
+assert_status "unsupported status control char status" 1 "$LAST_STATUS"
+assert_eq "unsupported status control char stderr" \
+ "stty: argument 'status' is not supported on Linux: Linux termios has no VSTATUS control character" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" altwerase
+assert_status "unsupported altwerase status" 1 "$LAST_STATUS"
+assert_eq "unsupported altwerase stderr" \
+ "stty: argument 'altwerase' is not supported on Linux: Linux termios has no ALTWERASE local mode" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" kerninfo
+assert_status "unsupported kerninfo status" 1 "$LAST_STATUS"
+assert_eq "unsupported kerninfo stderr" \
+ "stty: argument 'kerninfo' is not supported on Linux: Linux has no STATUS control character or kernel tty status line" \
+ "$LAST_STDERR"
+
+run_capture "$STTY_BIN" -f "$SLAVE_PATH" rtsdtr
+assert_status "unsupported rtsdtr status" 1 "$LAST_STATUS"
+assert_eq "unsupported rtsdtr stderr" \
+ "stty: argument 'rtsdtr' is not supported on Linux: Linux termios cannot control BSD RTS/DTR-on-open semantics" \
+ "$LAST_STDERR"
+
+printf '%s\n' "PASS"