summaryrefslogtreecommitdiff
path: root/corebinutils
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:29:55 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:29:55 +0300
commit22343fc2bb8db94066d3e314c5a20a9b9278f4c9 (patch)
treed2a731892b60e63970796b567a6a2dd7eb209427 /corebinutils
parent4cf86e37e42f9f04ec4a41a28ffe466d2510438a (diff)
parentceefe27a76f3b2075abbf01b0c44375363967af6 (diff)
downloadProject-Tick-22343fc2bb8db94066d3e314c5a20a9b9278f4c9.tar.gz
Project-Tick-22343fc2bb8db94066d3e314c5a20a9b9278f4c9.zip
Add 'corebinutils/test/' from commit 'ceefe27a76f3b2075abbf01b0c44375363967af6'
git-subtree-dir: corebinutils/test git-subtree-mainline: 4cf86e37e42f9f04ec4a41a28ffe466d2510438a git-subtree-split: ceefe27a76f3b2075abbf01b0c44375363967af6
Diffstat (limited to 'corebinutils')
-rw-r--r--corebinutils/test/.gitignore25
-rw-r--r--corebinutils/test/GNUmakefile46
-rw-r--r--corebinutils/test/LICENSE26
-rw-r--r--corebinutils/test/README.md47
-rw-r--r--corebinutils/test/test.1392
-rw-r--r--corebinutils/test/test.c679
-rw-r--r--corebinutils/test/tests/fd_helper.c111
-rw-r--r--corebinutils/test/tests/legacy_test.sh209
-rw-r--r--corebinutils/test/tests/test.sh479
9 files changed, 2014 insertions, 0 deletions
diff --git a/corebinutils/test/.gitignore b/corebinutils/test/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/test/.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/test/GNUmakefile b/corebinutils/test/GNUmakefile
new file mode 100644
index 0000000000..828204bb77
--- /dev/null
+++ b/corebinutils/test/GNUmakefile
@@ -0,0 +1,46 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS ?=
+CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_GNU_SOURCE
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror -Wpedantic
+LDFLAGS ?=
+LDLIBS ?=
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/test
+BRACKET_TARGET := $(OUTDIR)/[
+FD_HELPER := $(OBJDIR)/fd_helper
+OBJS := $(OBJDIR)/test.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET) $(BRACKET_TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(BRACKET_TARGET): $(TARGET) | dirs
+ ln -sf "test" "$@"
+
+$(OBJDIR)/test.o: $(CURDIR)/test.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/test.c" -o "$@"
+
+$(FD_HELPER): $(CURDIR)/tests/fd_helper.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) "$(CURDIR)/tests/fd_helper.c" -o "$@" $(LDFLAGS) $(LDLIBS)
+
+test: $(TARGET) $(BRACKET_TARGET) $(FD_HELPER)
+ TEST_BIN="$(TARGET)" BRACKET_BIN="$(BRACKET_TARGET)" \
+ FD_HELPER_BIN="$(FD_HELPER)" \
+ sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/test/LICENSE b/corebinutils/test/LICENSE
new file mode 100644
index 0000000000..c735a9a4d6
--- /dev/null
+++ b/corebinutils/test/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+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/test/README.md b/corebinutils/test/README.md
new file mode 100644
index 0000000000..cca3353d99
--- /dev/null
+++ b/corebinutils/test/README.md
@@ -0,0 +1,47 @@
+# test
+
+Standalone Linux-native port of FreeBSD `test(1)` / `[(1)` for Project Tick BSD/Linux Distribution.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Port Strategy
+
+- The standalone layout follows sibling ports such as `bin/expr` and `bin/pwd`: local `GNUmakefile`, short technical `README.md`, and shell regression tests under `tests/`.
+- The FreeBSD shared-source shape (`/bin/test` plus `/bin/sh` builtin hooks) was removed. This port is a standalone utility and keeps the expression parser in one Linux-focused source file.
+- The original recursive-descent parser and POSIX ambiguity handling are preserved, but the parser state is moved into an explicit `struct parser` instead of global cursor variables.
+- BSD libc-only diagnostics (`err(3)`, `__dead2`, `__printf0like`, `__nonstring`) are replaced with plain `fprintf(3)` / `vfprintf(3)` error paths so the code builds cleanly on glibc and musl.
+
+## Linux API Mapping
+
+| FreeBSD / BSD mechanism | Linux-native replacement | Reasoning |
+|---|---|---|
+| `eaccess(2)` for `-r`, `-w`, `-x` | `faccessat(AT_FDCWD, ..., AT_EACCESS)` | Linux exposes effective-ID permission checks through `faccessat` without a BSD shim layer |
+| `-e` existence check | `stat(2)` / `lstat(2)` result | existence does not need a separate access probe on Linux |
+| `err(3)` / `verrx(3)` | local `fprintf(3)` / `vfprintf(3)` diagnostics | removes BSD libc dependency and keeps exact exit status control |
+| shared `/bin/sh` builtin glue (`#ifdef SHELL`, `bltin.h`) | removed | target deliverable here is the standalone userland tool, not a shell builtin |
+| implicit parser globals (`nargc`, `t_wp`, `parenlevel`) | `struct parser` state object | reduces global state and makes ambiguity handling easier to audit |
+| `stat(2)` / `lstat(2)` timestamp compare with BSD struct access | Linux `st_mtim` comparison | POSIX.1-2008 / Linux-compatible nanosecond mtime comparison for `-nt` and `-ot` |
+
+## Supported Semantics
+
+- All primaries documented in [`test.1`](/home/samet/freebsd/bin/test/test.1), including `-L`, `-h`, `-S`, `-O`, `-G`, `-nt`, `-ot`, `-ef`, and the compatibility `==` operator.
+- Ambiguous POSIX grammar cases handled the same way as the historical FreeBSD parser, including the non-short-circuit `-a` / `-o` behaviour documented in `BUGS`.
+- `[` is built as a symlink to `test`; missing closing `]` is a hard error.
+- Numeric parsing is strict: trailing garbage is rejected, and values outside `intmax_t` or file-descriptor `int` range fail explicitly.
+
+## Intentionally Unsupported Semantics
+
+- The shared-source `/bin/sh` builtin integration is not carried into this standalone Linux port. That is a shell-porting concern, not a `test(1)` binary concern.
+- If the running Linux libc/kernel combination cannot provide `faccessat(..., AT_EACCESS)`, the port exits with an explicit error instead of silently approximating effective-ID permission checks.
diff --git a/corebinutils/test/test.1 b/corebinutils/test/test.1
new file mode 100644
index 0000000000..04a39a3c7e
--- /dev/null
+++ b/corebinutils/test/test.1
@@ -0,0 +1,392 @@
+.\"-
+.\" Copyright (c) 1991, 1993
+.\" The Regents of the University of California. 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 October 5, 2016
+.Dt TEST 1
+.Os
+.Sh NAME
+.Nm test ,
+.Nm \&[
+.Nd condition evaluation utility
+.Sh SYNOPSIS
+.Nm
+.Ar expression
+.Nm \&[
+.Ar expression Cm \&]
+.Sh DESCRIPTION
+The
+.Nm
+utility evaluates the expression and, if it evaluates
+to true, returns a zero (true) exit status; otherwise
+it returns 1 (false).
+If there is no expression,
+.Nm
+also
+returns 1 (false).
+.Pp
+All operators and flags are separate arguments to the
+.Nm
+utility.
+.Pp
+The following primaries are used to construct expression:
+.Bl -tag -width Ar
+.It Fl b Ar file
+True if
+.Ar file
+exists and is a block special
+file.
+.It Fl c Ar file
+True if
+.Ar file
+exists and is a character
+special file.
+.It Fl d Ar file
+True if
+.Ar file
+exists and is a directory.
+.It Fl e Ar file
+True if
+.Ar file
+exists (regardless of type).
+.It Fl f Ar file
+True if
+.Ar file
+exists and is a regular file.
+.It Fl g Ar file
+True if
+.Ar file
+exists and its set group ID flag
+is set.
+.It Fl h Ar file
+True if
+.Ar file
+exists and is a symbolic link.
+This operator is retained for compatibility with previous versions of
+this program.
+Do not rely on its existence; use
+.Fl L
+instead.
+.It Fl k Ar file
+True if
+.Ar file
+exists and its sticky bit is set.
+.It Fl n Ar string
+True if the length of
+.Ar string
+is nonzero.
+.It Fl p Ar file
+True if
+.Ar file
+is a named pipe
+.Pq Tn FIFO .
+.It Fl r Ar file
+True if
+.Ar file
+exists and is readable.
+.It Fl s Ar file
+True if
+.Ar file
+exists and has a size greater
+than zero.
+.It Fl t Ar file_descriptor
+True if the file whose file descriptor number
+is
+.Ar file_descriptor
+is open and is associated with a terminal.
+.It Fl u Ar file
+True if
+.Ar file
+exists and its set user ID flag
+is set.
+.It Fl w Ar file
+True if
+.Ar file
+exists and is writable.
+True
+indicates only that the write flag is on.
+The file is not writable on a read-only file
+system even if this test indicates true.
+.It Fl x Ar file
+True if
+.Ar file
+exists and is executable.
+True
+indicates only that the execute flag is on.
+If
+.Ar file
+is a directory, true indicates that
+.Ar file
+can be searched.
+.It Fl z Ar string
+True if the length of
+.Ar string
+is zero.
+.It Fl L Ar file
+True if
+.Ar file
+exists and is a symbolic link.
+.It Fl O Ar file
+True if
+.Ar file
+exists and its owner matches the effective user id of this process.
+.It Fl G Ar file
+True if
+.Ar file
+exists and its group matches the effective group id of this process.
+.It Fl S Ar file
+True if
+.Ar file
+exists and is a socket.
+.It Ar file1 Fl nt Ar file2
+True if
+.Ar file1
+exists and is newer than
+.Ar file2 .
+.It Ar file1 Fl ot Ar file2
+True if
+.Ar file1
+exists and is older than
+.Ar file2 .
+.It Ar file1 Fl ef Ar file2
+True if
+.Ar file1
+and
+.Ar file2
+exist and refer to the same file.
+.It Ar string
+True if
+.Ar string
+is not the null
+string.
+.It Ar s1 Cm = Ar s2
+True if the strings
+.Ar s1
+and
+.Ar s2
+are identical.
+.It Ar s1 Cm != Ar s2
+True if the strings
+.Ar s1
+and
+.Ar s2
+are not identical.
+.It Ar s1 Cm < Ar s2
+True if string
+.Ar s1
+comes before
+.Ar s2
+based on the binary value of their characters.
+.It Ar s1 Cm > Ar s2
+True if string
+.Ar s1
+comes after
+.Ar s2
+based on the binary value of their characters.
+.It Ar n1 Fl eq Ar n2
+True if the integers
+.Ar n1
+and
+.Ar n2
+are algebraically
+equal.
+.It Ar n1 Fl ne Ar n2
+True if the integers
+.Ar n1
+and
+.Ar n2
+are not
+algebraically equal.
+.It Ar n1 Fl gt Ar n2
+True if the integer
+.Ar n1
+is algebraically
+greater than the integer
+.Ar n2 .
+.It Ar n1 Fl ge Ar n2
+True if the integer
+.Ar n1
+is algebraically
+greater than or equal to the integer
+.Ar n2 .
+.It Ar n1 Fl lt Ar n2
+True if the integer
+.Ar n1
+is algebraically less
+than the integer
+.Ar n2 .
+.It Ar n1 Fl le Ar n2
+True if the integer
+.Ar n1
+is algebraically less
+than or equal to the integer
+.Ar n2 .
+.El
+.Pp
+If
+.Ar file
+is a symbolic link,
+.Nm
+will fully dereference it and then evaluate the expression
+against the file referenced, except for the
+.Fl h
+and
+.Fl L
+primaries.
+.Pp
+These primaries can be combined with the following operators:
+.Bl -tag -width Ar
+.It Cm \&! Ar expression
+True if
+.Ar expression
+is false.
+.It Ar expression1 Fl a Ar expression2
+True if both
+.Ar expression1
+and
+.Ar expression2
+are true.
+.It Ar expression1 Fl o Ar expression2
+True if either
+.Ar expression1
+or
+.Ar expression2
+are true.
+.It Cm \&( Ar expression Cm \&)
+True if expression is true.
+.El
+.Pp
+The
+.Fl a
+operator has higher precedence than the
+.Fl o
+operator.
+.Pp
+Some shells may provide a builtin
+.Nm
+command which is similar or identical to this utility.
+Consult the
+.Xr builtin 1
+manual page.
+.Sh GRAMMAR AMBIGUITY
+The
+.Nm
+grammar is inherently ambiguous.
+In order to assure a degree of consistency,
+the cases described in the
+.St -p1003.2 ,
+section D11.2/4.62.4, standard
+are evaluated consistently according to the rules specified in the
+standards document.
+All other cases are subject to the ambiguity in the
+command semantics.
+.Pp
+In particular, only expressions containing
+.Fl a ,
+.Fl o ,
+.Cm \&(
+or
+.Cm \&)
+can be ambiguous.
+.Sh EXIT STATUS
+The
+.Nm
+utility exits with one of the following values:
+.Bl -tag -width indent
+.It 0
+expression evaluated to true.
+.It 1
+expression evaluated to false or expression was
+missing.
+.It >1
+An error occurred.
+.El
+.Sh EXAMPLES
+Implement
+.Li test FILE1 -nt FILE2
+using only
+.Tn POSIX
+functionality:
+.Pp
+.Dl test -n \&"$(find -L -- FILE1 -prune -newer FILE2 2>/dev/null)\&"
+.Pp
+This can be modified using non-standard
+.Xr find 1
+primaries like
+.Cm -newerca
+to compare other timestamps.
+.Sh COMPATIBILITY
+For compatibility with some other implementations,
+the
+.Cm =
+primary can be substituted with
+.Cm ==
+with the same meaning.
+.Sh SEE ALSO
+.Xr builtin 1 ,
+.Xr expr 1 ,
+.Xr find 1 ,
+.Xr sh 1 ,
+.Xr stat 1 ,
+.Xr symlink 7
+.Sh STANDARDS
+The
+.Nm
+utility implements a superset of the
+.St -p1003.2
+specification.
+The primaries
+.Cm < ,
+.Cm == ,
+.Cm > ,
+.Fl ef ,
+.Fl nt ,
+.Fl ot ,
+.Fl G ,
+and
+.Fl O
+are extensions.
+.Sh HISTORY
+A
+.Nm
+utility appeared in
+.At v7 .
+.Sh BUGS
+Both sides are always evaluated in
+.Fl a
+and
+.Fl o .
+For instance, the writable status of
+.Pa file
+will be tested by the following command even though the former expression
+indicated false, which results in a gratuitous access to the file system:
+.Dl "[ -z abc -a -w file ]"
+To avoid this, write
+.Dl "[ -z abc ] && [ -w file ]"
diff --git a/corebinutils/test/test.c b/corebinutils/test/test.c
new file mode 100644
index 0000000000..8159a9f0fd
--- /dev/null
+++ b/corebinutils/test/test.c
@@ -0,0 +1,679 @@
+/*
+
+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.
+
+*/
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+enum token_type {
+ TOKEN_UNARY = 0x100,
+ TOKEN_BINARY = 0x200,
+ TOKEN_BOOLEAN_UNARY = 0x300,
+ TOKEN_BOOLEAN_BINARY = 0x400,
+ TOKEN_PAREN = 0x500
+};
+
+enum token {
+ TOKEN_EOI,
+ TOKEN_OPERAND,
+ TOKEN_FILRD = TOKEN_UNARY + 1,
+ TOKEN_FILWR,
+ TOKEN_FILEX,
+ TOKEN_FILEXIST,
+ TOKEN_FILREG,
+ TOKEN_FILDIR,
+ TOKEN_FILCDEV,
+ TOKEN_FILBDEV,
+ TOKEN_FILFIFO,
+ TOKEN_FILSOCK,
+ TOKEN_FILSYM,
+ TOKEN_FILGZ,
+ TOKEN_FILTT,
+ TOKEN_FILSUID,
+ TOKEN_FILSGID,
+ TOKEN_FILSTCK,
+ TOKEN_STREZ,
+ TOKEN_STRNZ,
+ TOKEN_FILUID,
+ TOKEN_FILGID,
+ TOKEN_FILNT = TOKEN_BINARY + 1,
+ TOKEN_FILOT,
+ TOKEN_FILEQ,
+ TOKEN_STREQ,
+ TOKEN_STRNE,
+ TOKEN_STRLT,
+ TOKEN_STRGT,
+ TOKEN_INTEQ,
+ TOKEN_INTNE,
+ TOKEN_INTGE,
+ TOKEN_INTGT,
+ TOKEN_INTLE,
+ TOKEN_INTLT,
+ TOKEN_UNOT = TOKEN_BOOLEAN_UNARY + 1,
+ TOKEN_BAND = TOKEN_BOOLEAN_BINARY + 1,
+ TOKEN_BOR,
+ TOKEN_LPAREN = TOKEN_PAREN + 1,
+ TOKEN_RPAREN
+};
+
+#define TOKEN_FAMILY(token) ((token) & 0xff00)
+
+struct operator {
+ const char *text;
+ enum token token;
+};
+
+struct parser {
+ char **argv;
+ int pos;
+ int remaining;
+ int paren_level;
+};
+
+static const struct operator ops_single[] = {
+ {"=", TOKEN_STREQ},
+ {"<", TOKEN_STRLT},
+ {">", TOKEN_STRGT},
+ {"!", TOKEN_UNOT},
+ {"(", TOKEN_LPAREN},
+ {")", TOKEN_RPAREN},
+};
+
+static const struct operator ops_dash_single[] = {
+ {"r", TOKEN_FILRD},
+ {"w", TOKEN_FILWR},
+ {"x", TOKEN_FILEX},
+ {"e", TOKEN_FILEXIST},
+ {"f", TOKEN_FILREG},
+ {"d", TOKEN_FILDIR},
+ {"c", TOKEN_FILCDEV},
+ {"b", TOKEN_FILBDEV},
+ {"p", TOKEN_FILFIFO},
+ {"u", TOKEN_FILSUID},
+ {"g", TOKEN_FILSGID},
+ {"k", TOKEN_FILSTCK},
+ {"s", TOKEN_FILGZ},
+ {"t", TOKEN_FILTT},
+ {"z", TOKEN_STREZ},
+ {"n", TOKEN_STRNZ},
+ {"h", TOKEN_FILSYM},
+ {"O", TOKEN_FILUID},
+ {"G", TOKEN_FILGID},
+ {"L", TOKEN_FILSYM},
+ {"S", TOKEN_FILSOCK},
+ {"a", TOKEN_BAND},
+ {"o", TOKEN_BOR},
+};
+
+static const struct operator ops_double[] = {
+ {"==", TOKEN_STREQ},
+ {"!=", TOKEN_STRNE},
+};
+
+static const struct operator ops_dash_double[] = {
+ {"eq", TOKEN_INTEQ},
+ {"ne", TOKEN_INTNE},
+ {"ge", TOKEN_INTGE},
+ {"gt", TOKEN_INTGT},
+ {"le", TOKEN_INTLE},
+ {"lt", TOKEN_INTLT},
+ {"nt", TOKEN_FILNT},
+ {"ot", TOKEN_FILOT},
+ {"ef", TOKEN_FILEQ},
+};
+
+static const char *program_name = "test";
+
+__attribute__((format(printf, 1, 2), noreturn))
+static void
+die(const char *fmt, ...)
+{
+ va_list ap;
+
+ va_start(ap, fmt);
+ fprintf(stderr, "%s: ", program_name);
+ vfprintf(stderr, fmt, ap);
+ fputc('\n', stderr);
+ va_end(ap);
+ exit(2);
+}
+
+static void
+syntax_error(const char *op, const char *msg)
+{
+ if (op != NULL && *op != '\0')
+ die("%s: %s", op, msg);
+ die("%s", msg);
+}
+
+static const char *
+base_name(const char *path)
+{
+ const char *slash;
+
+ slash = strrchr(path, '/');
+ return slash == NULL ? path : slash + 1;
+}
+
+static const char *
+current_arg(const struct parser *parser)
+{
+ if (parser->remaining <= 0)
+ return NULL;
+ return parser->argv[parser->pos];
+}
+
+static const char *
+peek_arg(const struct parser *parser, int offset)
+{
+ if (offset < 0 || offset >= parser->remaining)
+ return NULL;
+ return parser->argv[parser->pos + offset];
+}
+
+static const char *
+advance_arg(struct parser *parser)
+{
+ if (parser->remaining > 0) {
+ parser->pos++;
+ parser->remaining--;
+ }
+ return current_arg(parser);
+}
+
+static void
+rewind_arg(struct parser *parser)
+{
+ if (parser->pos > 0) {
+ parser->pos--;
+ parser->remaining++;
+ }
+}
+
+static enum token
+lookup_operator(const struct operator *ops, size_t count, const char *text)
+{
+ size_t i;
+
+ for (i = 0; i < count; i++) {
+ if (strcmp(text, ops[i].text) == 0)
+ return ops[i].token;
+ }
+ return TOKEN_OPERAND;
+}
+
+static enum token
+find_operator(const char *text)
+{
+ size_t length;
+
+ if (text == NULL || text[0] == '\0')
+ return TOKEN_OPERAND;
+
+ length = strlen(text);
+ if (length == 1)
+ return lookup_operator(ops_single,
+ sizeof(ops_single) / sizeof(ops_single[0]), text);
+ if (length == 2) {
+ if (text[0] == '-')
+ return lookup_operator(ops_dash_single,
+ sizeof(ops_dash_single) / sizeof(ops_dash_single[0]),
+ text + 1);
+ return lookup_operator(ops_double,
+ sizeof(ops_double) / sizeof(ops_double[0]), text);
+ }
+ if (length == 3 && text[0] == '-') {
+ return lookup_operator(ops_dash_double,
+ sizeof(ops_dash_double) / sizeof(ops_dash_double[0]),
+ text + 1);
+ }
+ return TOKEN_OPERAND;
+}
+
+static bool
+is_unary_operand(const struct parser *parser)
+{
+ const char *next;
+ const char *after_next;
+ enum token next_token;
+
+ if (parser->remaining == 1)
+ return true;
+
+ next = peek_arg(parser, 1);
+ if (parser->remaining == 2)
+ return parser->paren_level == 1 && strcmp(next, ")") == 0;
+
+ after_next = peek_arg(parser, 2);
+ next_token = find_operator(next);
+ return TOKEN_FAMILY(next_token) == TOKEN_BINARY &&
+ (parser->paren_level == 0 || strcmp(after_next, ")") != 0);
+}
+
+static bool
+is_left_paren_operand(const struct parser *parser)
+{
+ const char *next;
+ enum token next_token;
+
+ if (parser->remaining == 1)
+ return true;
+
+ next = peek_arg(parser, 1);
+ if (parser->remaining == 2)
+ return parser->paren_level == 1 && strcmp(next, ")") == 0;
+ if (parser->remaining != 3)
+ return false;
+
+ next_token = find_operator(next);
+ return TOKEN_FAMILY(next_token) == TOKEN_BINARY;
+}
+
+static bool
+is_right_paren_operand(const struct parser *parser)
+{
+ const char *next;
+
+ if (parser->remaining == 1)
+ return false;
+
+ next = peek_arg(parser, 1);
+ if (parser->remaining == 2)
+ return parser->paren_level == 1 && strcmp(next, ")") == 0;
+ return false;
+}
+
+static enum token
+lex_token(struct parser *parser, const char *text)
+{
+ enum token token;
+
+ if (text == NULL)
+ return TOKEN_EOI;
+
+ token = find_operator(text);
+ if (((TOKEN_FAMILY(token) == TOKEN_UNARY ||
+ TOKEN_FAMILY(token) == TOKEN_BOOLEAN_UNARY) &&
+ is_unary_operand(parser)) ||
+ (token == TOKEN_LPAREN && is_left_paren_operand(parser)) ||
+ (token == TOKEN_RPAREN && is_right_paren_operand(parser))) {
+ return TOKEN_OPERAND;
+ }
+ return token;
+}
+
+static int
+parse_int(const char *text)
+{
+ char *end;
+ intmax_t value;
+
+ errno = 0;
+ value = strtoimax(text, &end, 10);
+ if (end == text)
+ die("%s: bad number", text);
+ if (errno == ERANGE || value < INT_MIN || value > INT_MAX)
+ die("%s: out of range", text);
+
+ while (*end != '\0' && isspace((unsigned char)*end))
+ end++;
+ if (*end != '\0')
+ die("%s: bad number", text);
+
+ return (int)value;
+}
+
+static intmax_t
+parse_intmax(const char *text)
+{
+ char *end;
+ intmax_t value;
+
+ errno = 0;
+ value = strtoimax(text, &end, 10);
+ if (end == text)
+ die("%s: bad number", text);
+ if (errno == ERANGE)
+ die("%s: out of range", text);
+
+ while (*end != '\0' && isspace((unsigned char)*end))
+ end++;
+ if (*end != '\0')
+ die("%s: bad number", text);
+
+ return value;
+}
+
+static int
+effective_access(const char *path, int mode)
+{
+ if (faccessat(AT_FDCWD, path, mode, AT_EACCESS) == 0)
+ return 0;
+ if (errno == EINVAL || errno == ENOSYS)
+ die("Linux effective access checks require faccessat(AT_EACCESS)");
+ return -1;
+}
+
+static int
+compare_mtime(const struct stat *lhs, const struct stat *rhs)
+{
+ if (lhs->st_mtim.tv_sec > rhs->st_mtim.tv_sec)
+ return 1;
+ if (lhs->st_mtim.tv_sec < rhs->st_mtim.tv_sec)
+ return -1;
+ if (lhs->st_mtim.tv_nsec > rhs->st_mtim.tv_nsec)
+ return 1;
+ if (lhs->st_mtim.tv_nsec < rhs->st_mtim.tv_nsec)
+ return -1;
+ return 0;
+}
+
+static int
+newer_file(const char *lhs, const char *rhs)
+{
+ struct stat lhs_stat;
+ struct stat rhs_stat;
+
+ if (stat(lhs, &lhs_stat) != 0 || stat(rhs, &rhs_stat) != 0)
+ return 0;
+ return compare_mtime(&lhs_stat, &rhs_stat) > 0;
+}
+
+static int
+older_file(const char *lhs, const char *rhs)
+{
+ return newer_file(rhs, lhs);
+}
+
+static int
+same_file(const char *lhs, const char *rhs)
+{
+ struct stat lhs_stat;
+ struct stat rhs_stat;
+
+ return stat(lhs, &lhs_stat) == 0 &&
+ stat(rhs, &rhs_stat) == 0 &&
+ lhs_stat.st_dev == rhs_stat.st_dev &&
+ lhs_stat.st_ino == rhs_stat.st_ino;
+}
+
+static int
+evaluate_file_test(const char *path, enum token token)
+{
+ struct stat st;
+ int stat_result;
+
+ stat_result = token == TOKEN_FILSYM ? lstat(path, &st) : stat(path, &st);
+ if (stat_result != 0)
+ return 0;
+
+ switch (token) {
+ case TOKEN_FILRD:
+ return effective_access(path, R_OK) == 0;
+ case TOKEN_FILWR:
+ return effective_access(path, W_OK) == 0;
+ case TOKEN_FILEX:
+ if (effective_access(path, X_OK) != 0)
+ return 0;
+ if (S_ISDIR(st.st_mode) || geteuid() != 0)
+ return 1;
+ return (st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0;
+ case TOKEN_FILEXIST:
+ return 1;
+ case TOKEN_FILREG:
+ return S_ISREG(st.st_mode);
+ case TOKEN_FILDIR:
+ return S_ISDIR(st.st_mode);
+ case TOKEN_FILCDEV:
+ return S_ISCHR(st.st_mode);
+ case TOKEN_FILBDEV:
+ return S_ISBLK(st.st_mode);
+ case TOKEN_FILFIFO:
+ return S_ISFIFO(st.st_mode);
+ case TOKEN_FILSOCK:
+ return S_ISSOCK(st.st_mode);
+ case TOKEN_FILSYM:
+ return S_ISLNK(st.st_mode);
+ case TOKEN_FILSUID:
+ return (st.st_mode & S_ISUID) != 0;
+ case TOKEN_FILSGID:
+ return (st.st_mode & S_ISGID) != 0;
+ case TOKEN_FILSTCK:
+ return (st.st_mode & S_ISVTX) != 0;
+ case TOKEN_FILGZ:
+ return st.st_size > 0;
+ case TOKEN_FILUID:
+ return st.st_uid == geteuid();
+ case TOKEN_FILGID:
+ return st.st_gid == getegid();
+ default:
+ return 0;
+ }
+}
+
+static int
+compare_integers(const char *lhs, const char *rhs)
+{
+ intmax_t lhs_value;
+ intmax_t rhs_value;
+
+ lhs_value = parse_intmax(lhs);
+ rhs_value = parse_intmax(rhs);
+ if (lhs_value > rhs_value)
+ return 1;
+ if (lhs_value < rhs_value)
+ return -1;
+ return 0;
+}
+
+static int parse_oexpr(struct parser *parser, enum token token);
+
+static int
+parse_binop(struct parser *parser, enum token token)
+{
+ const char *lhs;
+ const char *op;
+ const char *rhs;
+
+ lhs = current_arg(parser);
+ advance_arg(parser);
+ op = current_arg(parser);
+ advance_arg(parser);
+ rhs = current_arg(parser);
+ if (rhs == NULL)
+ syntax_error(op, "argument expected");
+
+ switch (token) {
+ case TOKEN_STREQ:
+ return strcmp(lhs, rhs) == 0;
+ case TOKEN_STRNE:
+ return strcmp(lhs, rhs) != 0;
+ case TOKEN_STRLT:
+ return strcmp(lhs, rhs) < 0;
+ case TOKEN_STRGT:
+ return strcmp(lhs, rhs) > 0;
+ case TOKEN_INTEQ:
+ return compare_integers(lhs, rhs) == 0;
+ case TOKEN_INTNE:
+ return compare_integers(lhs, rhs) != 0;
+ case TOKEN_INTGE:
+ return compare_integers(lhs, rhs) >= 0;
+ case TOKEN_INTGT:
+ return compare_integers(lhs, rhs) > 0;
+ case TOKEN_INTLE:
+ return compare_integers(lhs, rhs) <= 0;
+ case TOKEN_INTLT:
+ return compare_integers(lhs, rhs) < 0;
+ case TOKEN_FILNT:
+ return newer_file(lhs, rhs);
+ case TOKEN_FILOT:
+ return older_file(lhs, rhs);
+ case TOKEN_FILEQ:
+ return same_file(lhs, rhs);
+ default:
+ abort();
+ }
+}
+
+static int
+parse_primary(struct parser *parser, enum token token)
+{
+ enum token next_token;
+ const char *operand;
+ int result;
+
+ if (token == TOKEN_EOI)
+ return 0;
+
+ if (token == TOKEN_LPAREN) {
+ parser->paren_level++;
+ next_token = lex_token(parser, advance_arg(parser));
+ if (next_token == TOKEN_RPAREN) {
+ parser->paren_level--;
+ return 0;
+ }
+ result = parse_oexpr(parser, next_token);
+ if (lex_token(parser, advance_arg(parser)) != TOKEN_RPAREN)
+ syntax_error(NULL, "closing paren expected");
+ parser->paren_level--;
+ return result;
+ }
+
+ if (TOKEN_FAMILY(token) == TOKEN_UNARY) {
+ if (parser->remaining <= 1)
+ syntax_error(NULL, "argument expected");
+ operand = advance_arg(parser);
+ switch (token) {
+ case TOKEN_STREZ:
+ return operand[0] == '\0';
+ case TOKEN_STRNZ:
+ return operand[0] != '\0';
+ case TOKEN_FILTT:
+ return isatty(parse_int(operand));
+ default:
+ return evaluate_file_test(operand, token);
+ }
+ }
+
+ next_token = lex_token(parser, peek_arg(parser, 1));
+ if (TOKEN_FAMILY(next_token) == TOKEN_BINARY)
+ return parse_binop(parser, next_token);
+
+ return current_arg(parser)[0] != '\0';
+}
+
+static int
+parse_nexpr(struct parser *parser, enum token token)
+{
+ if (token == TOKEN_UNOT)
+ return !parse_nexpr(parser, lex_token(parser, advance_arg(parser)));
+ return parse_primary(parser, token);
+}
+
+static int
+parse_aexpr(struct parser *parser, enum token token)
+{
+ int result;
+
+ result = parse_nexpr(parser, token);
+ if (lex_token(parser, advance_arg(parser)) == TOKEN_BAND)
+ return parse_aexpr(parser, lex_token(parser, advance_arg(parser))) &&
+ result;
+ rewind_arg(parser);
+ return result;
+}
+
+static int
+parse_oexpr(struct parser *parser, enum token token)
+{
+ int result;
+
+ result = parse_aexpr(parser, token);
+ if (lex_token(parser, advance_arg(parser)) == TOKEN_BOR)
+ return parse_oexpr(parser, lex_token(parser, advance_arg(parser))) ||
+ result;
+ rewind_arg(parser);
+ return result;
+}
+
+int
+main(int argc, char **argv)
+{
+ struct parser parser;
+ int result;
+
+ program_name = base_name(argv[0]);
+ if (strcmp(program_name, "[") == 0) {
+ if (argc == 1 || strcmp(argv[argc - 1], "]") != 0)
+ die("missing ']'");
+ argc--;
+ argv[argc] = NULL;
+ }
+
+ if (argc <= 1)
+ return 1;
+
+ parser.argv = argv + 1;
+ parser.pos = 0;
+ parser.remaining = argc - 1;
+ parser.paren_level = 0;
+
+ if (parser.remaining == 4 && strcmp(current_arg(&parser), "!") == 0) {
+ advance_arg(&parser);
+ result = parse_oexpr(&parser,
+ lex_token(&parser, current_arg(&parser)));
+ } else {
+ result = !parse_oexpr(&parser,
+ lex_token(&parser, current_arg(&parser)));
+ }
+
+ advance_arg(&parser);
+ if (parser.remaining > 0)
+ syntax_error(current_arg(&parser), "unexpected operator");
+
+ return result;
+}
diff --git a/corebinutils/test/tests/fd_helper.c b/corebinutils/test/tests/fd_helper.c
new file mode 100644
index 0000000000..27d81f7164
--- /dev/null
+++ b/corebinutils/test/tests/fd_helper.c
@@ -0,0 +1,111 @@
+/*
+
+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.
+
+*/
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static void
+die_errno(const char *what)
+{
+ fprintf(stderr, "fd_helper: %s: %s\n", what, strerror(errno));
+ exit(126);
+}
+
+static int
+parse_fd(const char *text)
+{
+ char *end;
+ long value;
+
+ errno = 0;
+ value = strtol(text, &end, 10);
+ if (end == text || *end != '\0' || errno == ERANGE ||
+ value < 0 || value > INT_MAX) {
+ fprintf(stderr, "fd_helper: invalid file descriptor: %s\n", text);
+ exit(126);
+ }
+ return (int)value;
+}
+
+int
+main(int argc, char **argv)
+{
+ int fd;
+ int master_fd;
+ int slave_fd;
+ char *slave_name;
+
+ if (argc < 4) {
+ fprintf(stderr, "usage: fd_helper fd program arg ...\n");
+ return 126;
+ }
+
+ fd = parse_fd(argv[1]);
+
+ master_fd = posix_openpt(O_RDWR | O_NOCTTY);
+ if (master_fd < 0)
+ die_errno("posix_openpt");
+ if (grantpt(master_fd) != 0)
+ die_errno("grantpt");
+ if (unlockpt(master_fd) != 0)
+ die_errno("unlockpt");
+
+ slave_name = ptsname(master_fd);
+ if (slave_name == NULL)
+ die_errno("ptsname");
+
+ slave_fd = open(slave_name, O_RDWR | O_NOCTTY);
+ if (slave_fd < 0)
+ die_errno("open slave pty");
+
+ if (dup2(slave_fd, fd) < 0)
+ die_errno("dup2");
+
+ if (!isatty(fd))
+ die_errno("isatty");
+
+ if (slave_fd != fd)
+ close(slave_fd);
+ close(master_fd);
+
+ execv(argv[2], &argv[2]);
+ die_errno("execv");
+}
diff --git a/corebinutils/test/tests/legacy_test.sh b/corebinutils/test/tests/legacy_test.sh
new file mode 100644
index 0000000000..94f51aefba
--- /dev/null
+++ b/corebinutils/test/tests/legacy_test.sh
@@ -0,0 +1,209 @@
+#!/bin/sh
+
+set -eu
+
+#-
+# Copyright (c) June 1996 Wolfram Schneider <wosch@FreeBSD.org>. Berlin.
+# All rights reserved.
+#
+# 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.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+
+#
+# TEST.sh - check if test(1) or builtin test works
+#
+
+# force a specified test program, e.g. `env TEST_BIN=/bin/test sh legacy_test.sh'
+TEST_BIN=${TEST_BIN:-test}
+FAILED=0
+
+t ()
+{
+ # $1 -> exit code
+ # $2 -> $test expression
+
+ count=$((count+1))
+ # check for syntax errors
+ if syntax=$(eval "\"$TEST_BIN\" $2" 2>&1); then
+ ret=0
+ else
+ ret=$?
+ fi
+ if test -n "$syntax"; then
+ printf "not ok %s - (syntax error)\n" "$count $2"
+ FAILED=1
+ elif [ "$ret" != "$1" ]; then
+ printf "not ok %s - (got $ret, expected $1)\n" "$count $2"
+ FAILED=1
+ else
+ printf "ok %s\n" "$count $2"
+ fi
+}
+
+count=0
+echo "1..130"
+
+t 0 'b = b'
+t 0 'b == b'
+t 1 'b != b'
+t 0 '\( b = b \)'
+t 0 '\( b == b \)'
+t 1 '! \( b = b \)'
+t 1 '! \( b == b \)'
+t 1 '! -f /etc/passwd'
+
+t 0 '-h = -h'
+t 0 '-o = -o'
+t 1 '-f = h'
+t 1 '-h = f'
+t 1 '-o = f'
+t 1 'f = -o'
+t 0 '\( -h = -h \)'
+t 1 '\( a = -h \)'
+t 1 '\( -f = h \)'
+t 0 '-h = -h -o a'
+t 0 '\( -h = -h \) -o 1'
+t 0 '-h = -h -o -h = -h'
+t 0 '\( -h = -h \) -o \( -h = -h \)'
+t 0 'roedelheim = roedelheim'
+t 1 'potsdam = berlin-dahlem'
+
+t 0 '-d /'
+t 0 '-d / -a a != b'
+t 1 '-z "-z"'
+t 0 '-n -n'
+
+t 0 '0'
+t 0 '\( 0 \)'
+t 0 '-E'
+t 0 '-X -a -X'
+t 0 '-XXX'
+t 0 '\( -E \)'
+t 0 'true -o X'
+t 0 'true -o -X'
+t 0 '\( \( \( a = a \) -o 1 \) -a 1 \) -a true'
+t 1 '-h /'
+t 0 '-r /'
+t 1 '-w /'
+t 0 '-x /bin/sh'
+t 0 '-c /dev/null'
+t 0 '-f /etc/passwd'
+t 0 '-s /etc/passwd'
+
+t 1 '! \( 700 -le 1000 -a -n "1" -a "20" = "20" \)'
+t 0 '100 -eq 100'
+t 0 '100 -lt 200'
+t 1 '1000 -lt 200'
+t 0 '1000 -gt 200'
+t 0 '1000 -ge 200'
+t 0 '1000 -ge 1000'
+t 1 '2 -ne 2'
+t 0 '0 -eq 0'
+t 1 '-5 -eq 5'
+t 0 '\( 0 -eq 0 \)'
+t 1 '1 -eq 0 -o a = a -a 1 -eq 0 -o a = aa'
+
+t 1 '"" -o ""'
+t 1 '"" -a ""'
+t 1 '"a" -a ""'
+t 0 '"a" -a ! ""'
+t 1 '""'
+t 0 '! ""'
+
+t 0 '!'
+t 0 '\('
+t 0 '\)'
+
+t 1 '\( = \)'
+t 0 '\( != \)'
+t 0 '\( ! \)'
+t 0 '\( \( \)'
+t 0 '\( \) \)'
+t 0 '! = !'
+t 1 '! != !'
+t 1 '-n = \)'
+t 0 '! != \)'
+t 1 '! = a'
+t 0 '! != -n'
+t 0 '! -c /etc/passwd'
+
+t 1 '! = = ='
+t 0 '! = = \)'
+t 0 '! "" -o ""'
+t 1 '! "x" -o ""'
+t 1 '! "" -o "x"'
+t 1 '! "x" -o "x"'
+t 0 '\( -f /etc/passwd \)'
+t 0 '\( ! "" \)'
+t 1 '\( ! -e \)'
+
+t 0 '0 -eq 0 -a -d /'
+t 0 '-s = "" -o "" = ""'
+t 0 '"" = "" -o -s = ""'
+t 1 '-s = "" -o -s = ""'
+t 0 '-z x -o x = "#" -o x = x'
+t 1 '-z y -o y = "#" -o y = x'
+t 0 '0 -ne 0 -o ! -f /'
+t 0 '1 -ne 0 -o ! -f /etc/passwd'
+t 1 '0 -ne 0 -o ! -f /etc/passwd'
+
+t 0 '-n ='
+t 1 '-z ='
+t 1 '! ='
+t 0 '-n -eq'
+t 1 '-z -eq'
+t 1 '! -eq'
+t 0 '-n -a'
+t 1 '-z -a'
+t 1 '! -a'
+t 0 '-n -o'
+t 1 '-z -o'
+t 1 '! -o'
+t 1 '! -n ='
+t 0 '! -z ='
+t 0 '! ! ='
+t 1 '! -n -eq'
+t 0 '! -z -eq'
+t 0 '! ! -eq'
+t 1 '! -n -a'
+t 0 '! -z -a'
+t 0 '! ! -a'
+t 1 '! -n -o'
+t 0 '! -z -o'
+t 0 '! ! -o'
+t 0 '\( -n = \)'
+t 1 '\( -z = \)'
+t 1 '\( ! = \)'
+t 0 '\( -n -eq \)'
+t 1 '\( -z -eq \)'
+t 1 '\( ! -eq \)'
+t 0 '\( -n -a \)'
+t 1 '\( -z -a \)'
+t 1 '\( ! -a \)'
+t 0 '\( -n -o \)'
+t 1 '\( -z -o \)'
+t 1 '\( ! -o \)'
+
+if [ "$FAILED" -ne 0 ]; then
+ exit 1
+fi
+
+printf '%s\n' "PASS"
diff --git a/corebinutils/test/tests/test.sh b/corebinutils/test/tests/test.sh
new file mode 100644
index 0000000000..debbbf81a1
--- /dev/null
+++ b/corebinutils/test/tests/test.sh
@@ -0,0 +1,479 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+TEST_BIN=${TEST_BIN:-"$ROOT/out/test"}
+BRACKET_BIN=${BRACKET_BIN:-"$ROOT/out/["}
+FD_HELPER_BIN=${FD_HELPER_BIN:-"$ROOT/build/fd_helper"}
+SHELL_BIN=$(command -v sh)
+
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/test-test.XXXXXX")
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+LAST_STATUS=0
+LAST_STDOUT=
+LAST_STDERR=
+
+export LC_ALL=C
+export TZ=UTC
+
+cleanup() {
+ rm -rf "$WORKDIR"
+}
+
+trap cleanup EXIT INT TERM HUP
+
+fail() {
+ printf 'FAIL: %s\n' "$1" >&2
+ exit 1
+}
+
+assert_status() {
+ name=$1
+ expected=$2
+ actual=$3
+
+ if [ "$expected" -ne "$actual" ]; then
+ printf 'FAIL: %s\n' "$name" >&2
+ printf 'expected status: %s\n' "$expected" >&2
+ printf 'actual status: %s\n' "$actual" >&2
+ exit 1
+ fi
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+
+ if [ "$expected" != "$actual" ]; then
+ printf 'FAIL: %s\n' "$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_empty() {
+ name=$1
+ value=$2
+
+ if [ -n "$value" ]; then
+ printf 'FAIL: %s\n' "$name" >&2
+ printf '%s\n' '--- expected empty ---' >&2
+ printf '%s\n' '--- actual ---' >&2
+ printf '%s\n' "$value" >&2
+ exit 1
+ fi
+}
+
+assert_contains() {
+ name=$1
+ value=$2
+ pattern=$3
+
+ case $value in
+ *"$pattern"*) ;;
+ *) fail "$name" ;;
+ esac
+}
+
+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")
+}
+
+run_in_shell() {
+ if sh -c "$1" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ LAST_STATUS=0
+ else
+ LAST_STATUS=$?
+ fi
+ LAST_STDOUT=$(cat "$STDOUT_FILE")
+ LAST_STDERR=$(cat "$STDERR_FILE")
+}
+
+find_socket_path() {
+ for candidate in \
+ /run/* /run/*/* /run/*/*/* \
+ /var/run/* /var/run/*/* /var/run/*/*/* \
+ /tmp/* /tmp/*/* /tmp/*/*/*; do
+ if [ -S "$candidate" ]; then
+ printf '%s\n' "$candidate"
+ return 0
+ fi
+ done
+ return 1
+}
+
+skip() {
+ printf 'SKIP: %s\n' "$1"
+}
+
+[ -x "$TEST_BIN" ] || fail "missing binary: $TEST_BIN"
+[ -x "$BRACKET_BIN" ] || fail "missing bracket binary: $BRACKET_BIN"
+[ -x "$FD_HELPER_BIN" ] || fail "missing fd helper: $FD_HELPER_BIN"
+
+run_capture "$TEST_BIN"
+assert_status "no expression status" 1 "$LAST_STATUS"
+assert_empty "no expression stdout" "$LAST_STDOUT"
+assert_empty "no expression stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" ""
+assert_status "empty operand status" 1 "$LAST_STATUS"
+assert_empty "empty operand stdout" "$LAST_STDOUT"
+assert_empty "empty operand stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" value
+assert_status "single operand status" 0 "$LAST_STATUS"
+assert_empty "single operand stdout" "$LAST_STDOUT"
+assert_empty "single operand stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -n ""
+assert_status "string -n status" 1 "$LAST_STATUS"
+assert_empty "string -n stdout" "$LAST_STDOUT"
+assert_empty "string -n stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -z ""
+assert_status "string -z status" 0 "$LAST_STATUS"
+assert_empty "string -z stdout" "$LAST_STDOUT"
+assert_empty "string -z stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -h = -h
+assert_status "operator-like operand status" 0 "$LAST_STATUS"
+assert_empty "operator-like operand stdout" "$LAST_STDOUT"
+assert_empty "operator-like operand stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" alpha = alpha
+assert_status "string equality status" 0 "$LAST_STATUS"
+assert_empty "string equality stdout" "$LAST_STDOUT"
+assert_empty "string equality stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" alpha == alpha
+assert_status "string double equals status" 0 "$LAST_STATUS"
+assert_empty "string double equals stdout" "$LAST_STDOUT"
+assert_empty "string double equals stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" alpha '!=' beta
+assert_status "string inequality status" 0 "$LAST_STATUS"
+assert_empty "string inequality stdout" "$LAST_STDOUT"
+assert_empty "string inequality stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" alpha '<' beta
+assert_status "string less-than status" 0 "$LAST_STATUS"
+assert_empty "string less-than stdout" "$LAST_STDOUT"
+assert_empty "string less-than stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" beta '>' alpha
+assert_status "string greater-than status" 0 "$LAST_STATUS"
+assert_empty "string greater-than stdout" "$LAST_STDOUT"
+assert_empty "string greater-than stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" 100 -eq 100
+assert_status "numeric eq status" 0 "$LAST_STATUS"
+assert_empty "numeric eq stdout" "$LAST_STDOUT"
+assert_empty "numeric eq stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -5 -lt 5
+assert_status "numeric lt status" 0 "$LAST_STATUS"
+assert_empty "numeric lt stdout" "$LAST_STDOUT"
+assert_empty "numeric lt stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" 7 -ge 8
+assert_status "numeric false status" 1 "$LAST_STATUS"
+assert_empty "numeric false stdout" "$LAST_STDOUT"
+assert_empty "numeric false stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" abc -eq 1
+assert_status "bad number status" 2 "$LAST_STATUS"
+assert_empty "bad number stdout" "$LAST_STDOUT"
+assert_eq "bad number stderr" "test: abc: bad number" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" 999999999999999999999999999999 -eq 1
+assert_status "out of range status" 2 "$LAST_STATUS"
+assert_empty "out of range stdout" "$LAST_STDOUT"
+assert_eq "out of range stderr" "test: 999999999999999999999999999999: out of range" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" '!' ""
+assert_status "bang empty status" 0 "$LAST_STATUS"
+assert_empty "bang empty stdout" "$LAST_STDOUT"
+assert_empty "bang empty stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" '!'
+assert_status "bare bang status" 0 "$LAST_STATUS"
+assert_empty "bare bang stdout" "$LAST_STDOUT"
+assert_empty "bare bang stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" '(' 0 -eq 0 ')' -a '(' 2 -gt 1 ')'
+assert_status "paren and status" 0 "$LAST_STATUS"
+assert_empty "paren and stdout" "$LAST_STDOUT"
+assert_empty "paren and stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" 1 -eq 0 -o a = a -a 1 -eq 0 -o a = aa
+assert_status "precedence status" 1 "$LAST_STATUS"
+assert_empty "precedence stdout" "$LAST_STDOUT"
+assert_empty "precedence stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" '(' 1 = 1
+assert_status "closing paren error status" 2 "$LAST_STATUS"
+assert_empty "closing paren error stdout" "$LAST_STDOUT"
+assert_eq "closing paren error stderr" "test: closing paren expected" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" 1 -eq
+assert_status "argument expected status" 2 "$LAST_STATUS"
+assert_empty "argument expected stdout" "$LAST_STDOUT"
+assert_eq "argument expected stderr" "test: -eq: argument expected" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" one two
+assert_status "unexpected operator status" 2 "$LAST_STATUS"
+assert_empty "unexpected operator stdout" "$LAST_STDOUT"
+assert_eq "unexpected operator stderr" "test: two: unexpected operator" "$LAST_STDERR"
+
+REGULAR_FILE="$WORKDIR/regular"
+EMPTY_FILE="$WORKDIR/empty"
+EXECUTABLE_FILE="$WORKDIR/executable"
+PERM_FILE="$WORKDIR/no-perm"
+FIFO_PATH="$WORKDIR/fifo"
+LINK_PATH="$WORKDIR/link"
+HARDLINK_PATH="$WORKDIR/hardlink"
+DIR_PATH="$WORKDIR/dir"
+STICKY_DIR="$WORKDIR/sticky"
+OLDER_FILE="$WORKDIR/older"
+NEWER_FILE="$WORKDIR/newer"
+MODE_FILE="$WORKDIR/mode"
+
+printf 'payload\n' >"$REGULAR_FILE"
+: >"$EMPTY_FILE"
+printf '#!/bin/sh\nexit 0\n' >"$EXECUTABLE_FILE"
+chmod 0755 "$EXECUTABLE_FILE"
+: >"$PERM_FILE"
+chmod 0000 "$PERM_FILE"
+mkfifo "$FIFO_PATH"
+ln -s "$REGULAR_FILE" "$LINK_PATH"
+ln "$REGULAR_FILE" "$HARDLINK_PATH"
+mkdir "$DIR_PATH"
+mkdir "$STICKY_DIR"
+chmod 1777 "$STICKY_DIR"
+: >"$MODE_FILE"
+chmod 6755 "$MODE_FILE"
+: >"$OLDER_FILE"
+sleep 1
+: >"$NEWER_FILE"
+
+run_capture "$TEST_BIN" -e "$REGULAR_FILE"
+assert_status "file exists status" 0 "$LAST_STATUS"
+assert_empty "file exists stdout" "$LAST_STDOUT"
+assert_empty "file exists stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -f "$REGULAR_FILE"
+assert_status "regular file status" 0 "$LAST_STATUS"
+assert_empty "regular file stdout" "$LAST_STDOUT"
+assert_empty "regular file stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -d "$DIR_PATH"
+assert_status "directory status" 0 "$LAST_STATUS"
+assert_empty "directory stdout" "$LAST_STDOUT"
+assert_empty "directory stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -s "$REGULAR_FILE"
+assert_status "size greater than zero status" 0 "$LAST_STATUS"
+assert_empty "size greater than zero stdout" "$LAST_STDOUT"
+assert_empty "size greater than zero stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -s "$EMPTY_FILE"
+assert_status "size zero status" 1 "$LAST_STATUS"
+assert_empty "size zero stdout" "$LAST_STDOUT"
+assert_empty "size zero stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -p "$FIFO_PATH"
+assert_status "fifo status" 0 "$LAST_STATUS"
+assert_empty "fifo stdout" "$LAST_STDOUT"
+assert_empty "fifo stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -L "$LINK_PATH"
+assert_status "symlink L status" 0 "$LAST_STATUS"
+assert_empty "symlink L stdout" "$LAST_STDOUT"
+assert_empty "symlink L stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -h "$LINK_PATH"
+assert_status "symlink h status" 0 "$LAST_STATUS"
+assert_empty "symlink h stdout" "$LAST_STDOUT"
+assert_empty "symlink h stderr" "$LAST_STDERR"
+
+SOCKET_PATH=$(find_socket_path || true)
+if [ -n "$SOCKET_PATH" ]; then
+ run_capture "$TEST_BIN" -S "$SOCKET_PATH"
+ assert_status "socket status" 0 "$LAST_STATUS"
+ assert_empty "socket stdout" "$LAST_STDOUT"
+ assert_empty "socket stderr" "$LAST_STDERR"
+else
+ skip "socket positive test skipped because no UNIX socket path is visible"
+fi
+
+run_capture "$TEST_BIN" -O "$REGULAR_FILE"
+assert_status "owner matches euid status" 0 "$LAST_STATUS"
+assert_empty "owner matches euid stdout" "$LAST_STDOUT"
+assert_empty "owner matches euid stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -G "$REGULAR_FILE"
+assert_status "group matches egid status" 0 "$LAST_STATUS"
+assert_empty "group matches egid stdout" "$LAST_STDOUT"
+assert_empty "group matches egid stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -u "$MODE_FILE"
+assert_status "setuid bit status" 0 "$LAST_STATUS"
+assert_empty "setuid bit stdout" "$LAST_STDOUT"
+assert_empty "setuid bit stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -g "$MODE_FILE"
+assert_status "setgid bit status" 0 "$LAST_STATUS"
+assert_empty "setgid bit stdout" "$LAST_STDOUT"
+assert_empty "setgid bit stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -k "$STICKY_DIR"
+assert_status "sticky bit status" 0 "$LAST_STATUS"
+assert_empty "sticky bit stdout" "$LAST_STDOUT"
+assert_empty "sticky bit stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -r "$REGULAR_FILE"
+assert_status "readable status" 0 "$LAST_STATUS"
+assert_empty "readable stdout" "$LAST_STDOUT"
+assert_empty "readable stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -w "$REGULAR_FILE"
+assert_status "writable status" 0 "$LAST_STATUS"
+assert_empty "writable stdout" "$LAST_STDOUT"
+assert_empty "writable stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -x "$EXECUTABLE_FILE"
+assert_status "executable status" 0 "$LAST_STATUS"
+assert_empty "executable stdout" "$LAST_STDOUT"
+assert_empty "executable stderr" "$LAST_STDERR"
+
+if [ "$(id -u)" -ne 0 ]; then
+ run_capture "$TEST_BIN" -r "$PERM_FILE"
+ assert_status "unreadable status" 1 "$LAST_STATUS"
+ assert_empty "unreadable stdout" "$LAST_STDOUT"
+ assert_empty "unreadable stderr" "$LAST_STDERR"
+
+ run_capture "$TEST_BIN" -w "$PERM_FILE"
+ assert_status "unwritable status" 1 "$LAST_STATUS"
+ assert_empty "unwritable stdout" "$LAST_STDOUT"
+ assert_empty "unwritable stderr" "$LAST_STDERR"
+else
+ skip "permission-negative tests skipped for euid 0"
+fi
+
+run_capture "$TEST_BIN" "$NEWER_FILE" -nt "$OLDER_FILE"
+assert_status "newer-than status" 0 "$LAST_STATUS"
+assert_empty "newer-than stdout" "$LAST_STDOUT"
+assert_empty "newer-than stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" "$OLDER_FILE" -ot "$NEWER_FILE"
+assert_status "older-than status" 0 "$LAST_STATUS"
+assert_empty "older-than stdout" "$LAST_STDOUT"
+assert_empty "older-than stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" "$REGULAR_FILE" -ef "$HARDLINK_PATH"
+assert_status "same file status" 0 "$LAST_STATUS"
+assert_empty "same file stdout" "$LAST_STDOUT"
+assert_empty "same file stderr" "$LAST_STDERR"
+
+run_capture "$TEST_BIN" -c /dev/null
+assert_status "character device status" 0 "$LAST_STATUS"
+assert_empty "character device stdout" "$LAST_STDOUT"
+assert_empty "character device stderr" "$LAST_STDERR"
+
+BLOCK_DEVICE_FOUND=
+for candidate in /dev/* /dev/*/*; do
+ if [ -b "$candidate" ]; then
+ BLOCK_DEVICE_FOUND=$candidate
+ break
+ fi
+done
+if [ -n "${BLOCK_DEVICE_FOUND:-}" ]; then
+ run_capture "$TEST_BIN" -b "$BLOCK_DEVICE_FOUND"
+ assert_status "block device status" 0 "$LAST_STATUS"
+ assert_empty "block device stdout" "$LAST_STDOUT"
+ assert_empty "block device stderr" "$LAST_STDERR"
+else
+ skip "block-device positive test skipped because no block device is visible"
+fi
+
+run_capture "$TEST_BIN" -t 99
+assert_status "closed fd tty status" 1 "$LAST_STATUS"
+assert_empty "closed fd tty stdout" "$LAST_STDOUT"
+assert_empty "closed fd tty stderr" "$LAST_STDERR"
+
+run_capture "$FD_HELPER_BIN" 9 "$TEST_BIN" -t 9
+case $LAST_STATUS in
+ 0)
+ assert_empty "pty fd tty stdout" "$LAST_STDOUT"
+ assert_empty "pty fd tty stderr" "$LAST_STDERR"
+ ;;
+ 1)
+ run_capture "$FD_HELPER_BIN" 9 "$SHELL_BIN" -c 'test -t 9'
+ case $LAST_STATUS in
+ 1)
+ skip "pty-backed -t positive test skipped because shell test also reports non-tty"
+ ;;
+ 126)
+ case $LAST_STDERR in
+ *"posix_openpt"*|*"grantpt"*|*"unlockpt"*|*"ptsname"*|*"open slave pty"*|*"isatty"*)
+ skip "pty-backed -t positive test skipped because PTY checks are blocked"
+ ;;
+ *)
+ fail "pty helper unexpected failure: $LAST_STDERR"
+ ;;
+ esac
+ ;;
+ *)
+ fail "pty fd tty status mismatch: test returned 1 but shell test -t returned $LAST_STATUS"
+ ;;
+ esac
+ ;;
+ 126)
+ case $LAST_STDERR in
+ *"posix_openpt"*|*"grantpt"*|*"unlockpt"*|*"ptsname"*|*"open slave pty"*|*"isatty"*)
+ skip "pty-backed -t positive test skipped because PTY checks are blocked"
+ ;;
+ *)
+ fail "pty helper unexpected failure: $LAST_STDERR"
+ ;;
+ esac
+ ;;
+ *)
+ fail "pty helper unexpected status: $LAST_STATUS"
+ ;;
+esac
+
+run_capture "$BRACKET_BIN" alpha = alpha ']'
+assert_status "bracket true status" 0 "$LAST_STATUS"
+assert_empty "bracket true stdout" "$LAST_STDOUT"
+assert_empty "bracket true stderr" "$LAST_STDERR"
+
+run_capture "$BRACKET_BIN" ']'
+assert_status "bracket empty expression status" 1 "$LAST_STATUS"
+assert_empty "bracket empty expression stdout" "$LAST_STDOUT"
+assert_empty "bracket empty expression stderr" "$LAST_STDERR"
+
+run_capture "$BRACKET_BIN" alpha = alpha
+assert_status "missing closing bracket status" 2 "$LAST_STATUS"
+assert_empty "missing closing bracket stdout" "$LAST_STDOUT"
+assert_eq "missing closing bracket stderr" "[: missing ']'" "$LAST_STDERR"
+
+run_in_shell "TEST_BIN='$TEST_BIN' sh '$ROOT/tests/legacy_test.sh'"
+assert_status "legacy suite status" 0 "$LAST_STATUS"
+assert_contains "legacy suite stdout" "$LAST_STDOUT" "1..130"
+assert_contains "legacy suite stdout" "$LAST_STDOUT" "PASS"
+assert_empty "legacy suite stderr" "$LAST_STDERR"
+
+printf '%s\n' "PASS"