diff options
| author | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:35 +0300 |
|---|---|---|
| committer | Mehmet Samet Duman <yongdohyun@projecttick.org> | 2026-04-02 18:29:35 +0300 |
| commit | 1edb2cb7b8a720ba8ae4438947256515a93b7ca9 (patch) | |
| tree | 7428ee326d8d1f00331258b038a33d675f7e0459 /corebinutils | |
| parent | 3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7 (diff) | |
| parent | f59550da80c3bd763f1d9782b3b3a06f16826889 (diff) | |
| download | Project-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/.gitignore | 25 | ||||
| -rw-r--r-- | corebinutils/stty/GNUmakefile | 35 | ||||
| -rw-r--r-- | corebinutils/stty/LICENSE | 29 | ||||
| -rw-r--r-- | corebinutils/stty/README.md | 46 | ||||
| -rw-r--r-- | corebinutils/stty/TODO | 1 | ||||
| -rw-r--r-- | corebinutils/stty/stty.1 | 647 | ||||
| -rw-r--r-- | corebinutils/stty/stty.c | 1607 | ||||
| -rw-r--r-- | corebinutils/stty/tests/mkpty.c | 56 | ||||
| -rw-r--r-- | corebinutils/stty/tests/termios_probe.c | 226 | ||||
| -rw-r--r-- | corebinutils/stty/tests/test.sh | 343 |
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" |
