aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rw-r--r--bin/cp/tests/Makefile2
-rwxr-xr-xbin/cp/tests/cp_test.sh10
-rw-r--r--bin/cp/tests/sparse.c73
-rw-r--r--bin/date/date.181
-rw-r--r--bin/date/date.c148
-rwxr-xr-xbin/date/tests/format_string_test.sh2
-rw-r--r--bin/ln/tests/ln_test.sh2
-rw-r--r--bin/pwait/pwait.16
-rw-r--r--bin/pwait/pwait.c128
-rw-r--r--bin/pwait/tests/pwait_test.sh38
-rw-r--r--bin/rm/rm.c4
-rw-r--r--bin/sh/Makefile7
-rw-r--r--bin/sh/dot.profile2
-rw-r--r--bin/sh/jobs.c9
-rw-r--r--bin/sh/miscbltin.c83
-rw-r--r--bin/sh/parser.c126
-rw-r--r--bin/sh/sh.16
-rw-r--r--bin/sh/tests/builtins/Makefile2
-rw-r--r--bin/sh/tests/builtins/read11.021
-rw-r--r--bin/sh/tests/builtins/read12.032
-rw-r--r--bin/sh/tests/builtins/wait11.06
-rw-r--r--bin/sh/tests/execution/Makefile1
-rw-r--r--bin/sh/tests/execution/bg14.09
-rw-r--r--bin/sh/tests/parser/Makefile6
-rw-r--r--bin/sh/tests/parser/ps1-expand1.07
-rw-r--r--bin/sh/tests/parser/ps1-expand2.07
-rw-r--r--bin/sh/tests/parser/ps1-expand3.08
-rw-r--r--bin/sh/tests/parser/ps1-expand4.08
-rw-r--r--bin/sh/tests/parser/ps1-expand5.08
-rw-r--r--bin/sh/tests/parser/ps2-expand1.012
30 files changed, 615 insertions, 239 deletions
diff --git a/bin/cp/tests/Makefile b/bin/cp/tests/Makefile
index 3fa9ae8f0685..a1917ada8fbf 100644
--- a/bin/cp/tests/Makefile
+++ b/bin/cp/tests/Makefile
@@ -1,7 +1,5 @@
PACKAGE= tests
ATF_TESTS_SH= cp_test
-PROGS+= sparse
-BINDIR= ${TESTSDIR}
.include <bsd.test.mk>
diff --git a/bin/cp/tests/cp_test.sh b/bin/cp/tests/cp_test.sh
index 999993bfad67..b637f862b7d3 100755
--- a/bin/cp/tests/cp_test.sh
+++ b/bin/cp/tests/cp_test.sh
@@ -90,7 +90,7 @@ hardlink_body()
echo "foo" >foo
atf_check cp -l foo bar
atf_check -o inline:"foo\n" cat bar
- atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
+ atf_check test foo -ef bar
}
atf_test_case hardlink_exists
@@ -105,7 +105,7 @@ hardlink_exists_body()
echo "bar" >bar
atf_check -s not-exit:0 -e match:exists cp -l foo bar
atf_check -o inline:"bar\n" cat bar
- atf_check_not_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
+ atf_check test ! foo -ef bar
}
atf_test_case hardlink_exists_force
@@ -120,7 +120,7 @@ hardlink_exists_force_body()
echo "bar" >bar
atf_check cp -fl foo bar
atf_check -o inline:"foo\n" cat bar
- atf_check_equal "$(stat -f%d,%i foo)" "$(stat -f%d,%i bar)"
+ atf_check test foo -ef bar
}
atf_test_case matching_srctgt
@@ -384,12 +384,12 @@ samefile_body()
file_is_sparse()
{
- atf_check ${0%/*}/sparse "$1"
+ atf_check -o match:"^[0-9]+-[0-9]" stat -h "$1"
}
files_are_equal()
{
- atf_check_not_equal "$(stat -f%d,%i "$1")" "$(stat -f%d,%i "$2")"
+ atf_check test ! "$1" -ef "$2"
atf_check cmp "$1" "$2"
}
diff --git a/bin/cp/tests/sparse.c b/bin/cp/tests/sparse.c
deleted file mode 100644
index 78957581a56c..000000000000
--- a/bin/cp/tests/sparse.c
+++ /dev/null
@@ -1,73 +0,0 @@
-/*-
- * Copyright (c) 2023 Klara, Inc.
- *
- * SPDX-License-Identifier: BSD-2-Clause
- */
-
-#include <err.h>
-#include <fcntl.h>
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <sysexits.h>
-#include <unistd.h>
-
-static bool verbose;
-
-/*
- * Returns true if the file named by its argument is sparse, i.e. if
- * seeking to SEEK_HOLE returns a different value than seeking to
- * SEEK_END.
- */
-static bool
-sparse(const char *filename)
-{
- off_t hole, end;
- int fd;
-
- if ((fd = open(filename, O_RDONLY)) < 0 ||
- (hole = lseek(fd, 0, SEEK_HOLE)) < 0 ||
- (end = lseek(fd, 0, SEEK_END)) < 0)
- err(1, "%s", filename);
- close(fd);
- if (end > hole) {
- if (verbose)
- printf("%s: hole at %zu\n", filename, (size_t)hole);
- return (true);
- }
- return (false);
-}
-
-static void
-usage(void)
-{
-
- fprintf(stderr, "usage: sparse [-v] file [...]\n");
- exit(EX_USAGE);
-}
-
-int
-main(int argc, char *argv[])
-{
- int opt, rv;
-
- while ((opt = getopt(argc, argv, "v")) != -1) {
- switch (opt) {
- case 'v':
- verbose = true;
- break;
- default:
- usage();
- break;
- }
- }
- argc -= optind;
- argv += optind;
- if (argc == 0)
- usage();
- rv = EXIT_SUCCESS;
- while (argc-- > 0)
- if (!sparse(*argv++))
- rv = EXIT_FAILURE;
- exit(rv);
-}
diff --git a/bin/date/date.1 b/bin/date/date.1
index b86a660a924d..374a687fcbdc 100644
--- a/bin/date/date.1
+++ b/bin/date/date.1
@@ -29,7 +29,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd September 1, 2025
+.Dd November 10, 2025
.Dt DATE 1
.Os
.Sh NAME
@@ -143,7 +143,8 @@ values are
.Cm minutes ,
.Cm seconds ,
and
-.Cm ns No Pq for nanoseconds .
+.Cm ns
+.Pq for nanoseconds .
The date and time is formatted to the specified precision.
When
.Ar FMT
@@ -172,7 +173,7 @@ Obsolete flag, accepted and ignored for compatibility.
.It Fl R
Use RFC 2822 date and time output format.
This is equivalent to using
-.Dq Li %a, %d %b %Y \&%T %z
+.Ql %a, %d %b %Y \&%T %z
as
.Ar output_fmt
while
@@ -185,7 +186,7 @@ Print the date and time represented by
.Ar seconds ,
where
.Ar seconds
-is the number of seconds since the Epoch
+is the number of seconds since the Unix Epoch
(00:00:00 UTC, January 1, 1970;
see
.Xr time 3 ) ,
@@ -194,9 +195,7 @@ and can be specified in decimal, octal, or hex.
Print the date and time of the last modification of
.Ar filename .
.It Fl u
-Display or set the date in
-.Tn UTC
-(Coordinated Universal) time.
+Display or set the date in UTC (Coordinated Universal) time.
By default
.Nm
displays the time in the time zone described by
@@ -322,20 +321,43 @@ Refer to the examples below for further details.
.Pp
An operand with a leading plus
.Pq Sq +
-sign signals a user-defined format string
+sign specifies a user-defined format string
which specifies the format in which to display the date and time.
The format string may contain any of the conversion specifications
described in the
.Xr strftime 3
-manual page and
-.Ql %N
-for nanoseconds, as well as any arbitrary text.
+manual page, as well as any arbitrary text.
+.Pp
+The following extensions to the regular
+.Xr strftime 3
+syntax are supported:
+.Bl -tag -width "xxxx"
+.It Cm \&% Ns Ar n Ns Cm N
+Replaced by the
+.Ar n Ns
+-digit fractional part of the number of seconds since the Unix Epoch.
+If
+.Ar n
+is omitted or zero, a default value of 9 is used, resulting in a
+number with nanosecond resolution (hence the choice of the letter
+.Sq N
+for this conversion).
+Note that the underlying clock may not necessarily support nanosecond
+resolution.
+.It Cm \&%-N
+As above, but automatically choose the precision based on the reported
+resolution of the underlying clock.
+If the
+.Fl r
+option was specified, the default precision of 9 digits is used.
+.El
+.Pp
A newline
.Pq Ql \en
character is always output after the characters specified by
the format string.
The format string for the default display is
-.Dq +%+ .
+.Dq %+ .
.Pp
If an operand does not have a leading plus sign, it is interpreted as
a value for setting the system's notion of the current date and time.
@@ -449,6 +471,13 @@ The
utility exits 0 on success, 1 if unable to set the date, and 2
if able to set the local date, but unable to set it globally.
.Sh EXAMPLES
+The command
+.Pp
+.Dl "date +%s.%3N"
+.Pp
+will print the time elapsed since the Unix Epoch with millisecond
+precision.
+.Pp
The command:
.Pp
.Dl "date ""+DATE: %Y-%m-%d%nTIME: %H:%M:%S"""
@@ -468,7 +497,7 @@ will display:
.Dl "Sun Jan 4 04:15:24 GMT 1998"
.Pp
where it is currently
-.Li "Mon Aug 4 04:15:24 BST 1997" .
+.Ql "Mon Aug 4 04:15:24 BST 1997" .
.Pp
The command:
.Pp
@@ -493,29 +522,31 @@ will display the last Friday of the month:
.Dl "Fri Aug 29 04:31:11 BST 1997"
.Pp
where it is currently
-.Li "Mon Aug 4 04:31:11 BST 1997" .
+.Ql "Mon Aug 4 04:31:11 BST 1997" .
.Pp
The command:
.Pp
.Dl "date 8506131627"
.Pp
sets the date to
-.Dq Li "June 13, 1985, 4:27 PM" .
+.Ql "June 13, 1985, 4:27 PM" .
.Pp
.Dl "date ""+%Y%m%d%H%M.%S"""
.Pp
may be used on one machine to print out the date
suitable for setting on another.
-.Qq ( Li "+%m%d%H%M%Y.%S"
-for use on
-.Tn Linux . )
+.Po Use
+.Ql "+%m%d%H%M%Y.%S"
+with GNU date on
+Linux .
+.Pc
.Pp
The command:
.Pp
.Dl "date 1432"
.Pp
sets the time to
-.Li "2:32 PM" ,
+.Ql "2:32 PM" ,
without modifying the date.
.Pp
The command
@@ -591,10 +622,10 @@ flag is compatible with
.St -iso8601 .
.Pp
The
-.Ql %N
+.Ql \&%N
conversion specification for nanoseconds is a non-standard extension.
It is compatible with GNU date's
-.Ql %N .
+.Ql \&%N .
.Sh HISTORY
A
.Nm
@@ -615,6 +646,12 @@ flag was added in
.Fx 12.0 .
.Pp
The
-.Ql %N
+.Ql \&%N
conversion specification was added in
.Fx 14.1 .
+Support for the
+.Ql \&% Ns Ar n Ns Cm N
+and
+.Ql \&%-N
+variants was added in
+.Fx 15.1 .
diff --git a/bin/date/date.c b/bin/date/date.c
index 01797084c0d6..9a40ac43d58b 100644
--- a/bin/date/date.c
+++ b/bin/date/date.c
@@ -36,6 +36,7 @@
#include <ctype.h>
#include <err.h>
#include <errno.h>
+#include <inttypes.h>
#include <locale.h>
#include <stdbool.h>
#include <stdio.h>
@@ -55,10 +56,10 @@ static void badformat(void);
static void iso8601_usage(const char *) __dead2;
static void multipleformats(void);
static void printdate(const char *);
-static void printisodate(struct tm *, long);
+static void printisodate(struct tm *, long, long);
static void setthetime(const char *, const char *, int, struct timespec *);
static size_t strftime_ns(char * __restrict, size_t, const char * __restrict,
- const struct tm * __restrict, long);
+ const struct tm * __restrict, long, long);
static void usage(void) __dead2;
static const struct iso8601_fmt {
@@ -78,26 +79,24 @@ static const char *rfc2822_format = "%a, %d %b %Y %T %z";
int
main(int argc, char *argv[])
{
- struct timespec ts;
+ struct timespec ts = { 0, 0 }, tres = { 0, 1 };
int ch, rflag;
bool Iflag, jflag, Rflag;
const char *format;
char buf[1024];
- char *fmt, *outzone = NULL;
- char *tmp;
+ char *end, *fmt, *outzone = NULL;
struct vary *v;
const struct vary *badv;
struct tm *lt;
struct stat sb;
size_t i;
+ intmax_t number;
v = NULL;
fmt = NULL;
(void) setlocale(LC_TIME, "");
rflag = 0;
Iflag = jflag = Rflag = 0;
- ts.tv_sec = 0;
- ts.tv_nsec = 0;
while ((ch = getopt(argc, argv, "f:I::jnRr:uv:z:")) != -1)
switch((char)ch) {
case 'f':
@@ -131,13 +130,15 @@ main(int argc, char *argv[])
break;
case 'r': /* user specified seconds */
rflag = 1;
- ts.tv_sec = strtoq(optarg, &tmp, 0);
- if (*tmp != 0) {
- if (stat(optarg, &sb) == 0) {
- ts.tv_sec = sb.st_mtim.tv_sec;
- ts.tv_nsec = sb.st_mtim.tv_nsec;
- } else
- usage();
+ number = strtoimax(optarg, &end, 0);
+ if (end > optarg && *end == '\0') {
+ ts.tv_sec = number;
+ ts.tv_nsec = 0;
+ } else if (stat(optarg, &sb) == 0) {
+ ts.tv_sec = sb.st_mtim.tv_sec;
+ ts.tv_nsec = sb.st_mtim.tv_nsec;
+ } else {
+ usage();
}
break;
case 'u': /* do everything in UTC */
@@ -155,8 +156,12 @@ main(int argc, char *argv[])
argc -= optind;
argv += optind;
- if (!rflag && clock_gettime(CLOCK_REALTIME, &ts) == -1)
- err(1, "clock_gettime");
+ if (!rflag) {
+ if (clock_gettime(CLOCK_REALTIME, &ts) == -1)
+ err(1, "clock_gettime");
+ if (clock_getres(CLOCK_REALTIME, &tres) == -1)
+ err(1, "clock_getres");
+ }
format = "%+";
@@ -191,14 +196,14 @@ main(int argc, char *argv[])
badv = vary_apply(v, lt);
if (badv) {
fprintf(stderr, "%s: Cannot apply date adjustment\n",
- badv->arg);
+ badv->arg);
vary_destroy(v);
usage();
}
vary_destroy(v);
if (Iflag)
- printisodate(lt, ts.tv_nsec);
+ printisodate(lt, ts.tv_nsec, tres.tv_nsec);
if (format == rfc2822_format)
/*
@@ -208,7 +213,8 @@ main(int argc, char *argv[])
setlocale(LC_TIME, "C");
- (void)strftime_ns(buf, sizeof(buf), format, lt, ts.tv_nsec);
+ (void)strftime_ns(buf, sizeof(buf), format, lt,
+ ts.tv_nsec, tres.tv_nsec);
printdate(buf);
}
@@ -222,7 +228,7 @@ printdate(const char *buf)
}
static void
-printisodate(struct tm *lt, long nsec)
+printisodate(struct tm *lt, long nsec, long res)
{
const struct iso8601_fmt *it;
char fmtbuf[64], buf[64], tzbuf[8];
@@ -231,10 +237,10 @@ printisodate(struct tm *lt, long nsec)
for (it = iso8601_fmts; it <= iso8601_selected; it++)
strlcat(fmtbuf, it->format_string, sizeof(fmtbuf));
- (void)strftime_ns(buf, sizeof(buf), fmtbuf, lt, nsec);
+ (void)strftime_ns(buf, sizeof(buf), fmtbuf, lt, nsec, res);
if (iso8601_selected > iso8601_fmts) {
- (void)strftime_ns(tzbuf, sizeof(tzbuf), "%z", lt, nsec);
+ (void)strftime_ns(tzbuf, sizeof(tzbuf), "%z", lt, nsec, res);
memmove(&tzbuf[4], &tzbuf[3], 3);
tzbuf[3] = ':';
strlcat(buf, tzbuf, sizeof(buf));
@@ -370,16 +376,17 @@ setthetime(const char *fmt, const char *p, int jflag, struct timespec *ts)
*/
static size_t
strftime_ns(char * __restrict s, size_t maxsize, const char * __restrict format,
- const struct tm * __restrict t, long nsec)
+ const struct tm * __restrict t, long nsec, long res)
{
- size_t prefixlen;
size_t ret;
char *newformat;
char *oldformat;
const char *prefix;
const char *suffix;
const char *tok;
- bool seen_percent;
+ long number;
+ int i, len, prefixlen, width, zeroes;
+ bool seen_percent, seen_dash, seen_width;
seen_percent = false;
if ((newformat = strdup(format)) == NULL)
@@ -392,36 +399,85 @@ strftime_ns(char * __restrict s, size_t maxsize, const char * __restrict format,
* If the previous token was a percent sign,
* then there are two percent tokens in a row.
*/
- if (seen_percent)
+ if (seen_percent) {
seen_percent = false;
- else
+ } else {
seen_percent = true;
+ seen_dash = seen_width = false;
+ prefixlen = tok - newformat;
+ width = 0;
+ }
break;
case 'N':
- if (seen_percent) {
- oldformat = newformat;
- prefix = oldformat;
- prefixlen = tok - oldformat - 1;
- suffix = tok + 1;
+ if (!seen_percent)
+ break;
+ oldformat = newformat;
+ prefix = oldformat;
+ suffix = tok + 1;
+ /*
+ * Prepare the number we are about to print. If
+ * the requested width is less than 9, we need to
+ * cut off the least significant digits. If it is
+ * more than 9, we will have to append zeroes.
+ */
+ if (seen_dash) {
/*
- * Construct a new format string from the
- * prefix (i.e., the part of the old format
- * from its beginning to the currently handled
- * "%N" conversion specification), the
- * nanoseconds, and the suffix (i.e., the part
- * of the old format from the next token to the
- * end).
+ * Calculate number of singificant digits
+ * based on res which is the clock's
+ * resolution in nanoseconds.
*/
- if (asprintf(&newformat, "%.*s%.9ld%s",
- (int)prefixlen, prefix, nsec,
- suffix) < 0) {
- err(1, "asprintf");
- }
- free(oldformat);
- tok = newformat + prefixlen + 9;
+ for (width = 9, number = res;
+ width > 0 && number > 0;
+ width--, number /= 10)
+ /* nothing */;
+ }
+ number = nsec;
+ zeroes = 0;
+ if (width == 0) {
+ width = 9;
+ } else if (width > 9) {
+ zeroes = width - 9;
+ width = 9;
+ } else {
+ for (i = 0; i < 9 - width; i++)
+ number /= 10;
}
+ /*
+ * Construct a new format string from the prefix
+ * (i.e., the part of the old format from its
+ * beginning to the currently handled "%N"
+ * conversion specification), the nanoseconds, and
+ * the suffix (i.e., the part of the old format
+ * from the next token to the end).
+ */
+ asprintf(&newformat, "%.*s%.*ld%.*d%n%s", prefixlen,
+ prefix, width, number, zeroes, 0, &len, suffix);
+ if (newformat == NULL)
+ err(1, "asprintf");
+ free(oldformat);
+ tok = newformat + len - 1;
seen_percent = false;
break;
+ case '-':
+ if (seen_percent) {
+ if (seen_dash || seen_width) {
+ seen_percent = false;
+ break;
+ }
+ seen_dash = true;
+ }
+ break;
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9':
+ if (seen_percent) {
+ if (seen_dash) {
+ seen_percent = false;
+ break;
+ }
+ width = width * 10 + *tok - '0';
+ seen_width = true;
+ }
+ break;
default:
seen_percent = false;
break;
diff --git a/bin/date/tests/format_string_test.sh b/bin/date/tests/format_string_test.sh
index c2fe2111373f..5f199a3b5fd6 100755
--- a/bin/date/tests/format_string_test.sh
+++ b/bin/date/tests/format_string_test.sh
@@ -132,6 +132,8 @@ atf_init_test_cases()
format_string_test M M 04 20
format_string_test m m 02 11
format_string_test N N 000000000 000000000
+ format_string_test 3N 3N 000 000
+ format_string_test 12N 12N 000000000000 000000000000
format_string_test p p AM PM
format_string_test R R 07:04 21:20
format_string_test r r "07:04:03 AM" "09:20:00 PM"
diff --git a/bin/ln/tests/ln_test.sh b/bin/ln/tests/ln_test.sh
index 78b4074aea18..ac9d785ba1fc 100644
--- a/bin/ln/tests/ln_test.sh
+++ b/bin/ln/tests/ln_test.sh
@@ -28,7 +28,7 @@
atf_check_same_file()
{
- atf_check_equal "$(stat -f %d,%i "$1")" "$(stat -f %d,%i "$2")"
+ atf_check test "$1" -ef "$2"
}
atf_check_symlink_to()
diff --git a/bin/pwait/pwait.1 b/bin/pwait/pwait.1
index 83ac8bcef317..d92b829b1d6a 100644
--- a/bin/pwait/pwait.1
+++ b/bin/pwait/pwait.1
@@ -30,7 +30,7 @@
.\" USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
.\" OF SUCH DAMAGE.
.\"
-.Dd January 21, 2021
+.Dd October 22, 2025
.Dt PWAIT 1
.Os
.Sh NAME
@@ -39,7 +39,7 @@
.Sh SYNOPSIS
.Nm
.Op Fl t Ar duration
-.Op Fl ov
+.Op Fl opv
.Ar pid
\&...
.Sh DESCRIPTION
@@ -51,6 +51,8 @@ The following option is available:
.Bl -tag -width indent
.It Fl o
Exit when any of the given processes has terminated.
+.It Fl p
+On exit, print a list of processes that have not terminated.
.It Fl t Ar duration
If any process is still running after
.Ar duration ,
diff --git a/bin/pwait/pwait.c b/bin/pwait/pwait.c
index 0fae22562607..59bf0eb93ced 100644
--- a/bin/pwait/pwait.c
+++ b/bin/pwait/pwait.c
@@ -33,23 +33,40 @@
#include <sys/types.h>
#include <sys/event.h>
+#include <sys/sysctl.h>
#include <sys/time.h>
+#include <sys/tree.h>
#include <sys/wait.h>
#include <err.h>
#include <errno.h>
#include <signal.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sysexits.h>
#include <unistd.h>
+struct pid {
+ RB_ENTRY(pid) entry;
+ pid_t pid;
+};
+
+static int
+pidcmp(const struct pid *a, const struct pid *b)
+{
+ return (a->pid > b->pid ? 1 : a->pid < b->pid ? -1 : 0);
+}
+
+RB_HEAD(pidtree, pid);
+static struct pidtree pids = RB_INITIALIZER(&pids);
+RB_GENERATE_STATIC(pidtree, pid, entry, pidcmp);
+
static void
usage(void)
{
-
- fprintf(stderr, "usage: pwait [-t timeout] [-ov] pid ...\n");
+ fprintf(stderr, "usage: pwait [-t timeout] [-opv] pid ...\n");
exit(EX_USAGE);
}
@@ -61,42 +78,55 @@ main(int argc, char *argv[])
{
struct itimerval itv;
struct kevent *e;
- int oflag, tflag, verbose;
- int i, kq, n, nleft, opt, status;
- long pid;
+ struct pid k, *p;
char *end, *s;
double timeout;
+ size_t sz;
+ long pid;
+ pid_t mypid;
+ int i, kq, n, ndone, nleft, opt, pid_max, ret, status;
+ bool oflag, pflag, tflag, verbose;
- oflag = 0;
- tflag = 0;
- verbose = 0;
+ oflag = false;
+ pflag = false;
+ tflag = false;
+ verbose = false;
memset(&itv, 0, sizeof(itv));
- while ((opt = getopt(argc, argv, "ot:v")) != -1) {
+ while ((opt = getopt(argc, argv, "opt:v")) != -1) {
switch (opt) {
case 'o':
- oflag = 1;
+ oflag = true;
+ break;
+ case 'p':
+ pflag = true;
break;
case 't':
- tflag = 1;
+ tflag = true;
errno = 0;
timeout = strtod(optarg, &end);
if (end == optarg || errno == ERANGE || timeout < 0) {
errx(EX_DATAERR, "timeout value");
}
- switch(*end) {
- case 0:
+ switch (*end) {
+ case '\0':
+ break;
case 's':
+ end++;
break;
case 'h':
timeout *= 60;
/* FALLTHROUGH */
case 'm':
timeout *= 60;
+ end++;
break;
default:
errx(EX_DATAERR, "timeout unit");
}
+ if (*end != '\0') {
+ errx(EX_DATAERR, "timeout unit");
+ }
if (timeout > 100000000L) {
errx(EX_DATAERR, "timeout value");
}
@@ -106,7 +136,7 @@ main(int argc, char *argv[])
(suseconds_t)(timeout * 1000000UL);
break;
case 'v':
- verbose = 1;
+ verbose = true;
break;
default:
usage();
@@ -121,53 +151,57 @@ main(int argc, char *argv[])
usage();
}
- kq = kqueue();
- if (kq == -1) {
+ if ((kq = kqueue()) < 0)
err(EX_OSERR, "kqueue");
- }
- e = malloc((argc + tflag) * sizeof(struct kevent));
- if (e == NULL) {
+ sz = sizeof(pid_max);
+ if (sysctlbyname("kern.pid_max", &pid_max, &sz, NULL, 0) != 0) {
+ pid_max = 99999;
+ }
+ if ((e = malloc((argc + tflag) * sizeof(*e))) == NULL) {
err(EX_OSERR, "malloc");
}
- nleft = 0;
+ ndone = nleft = 0;
+ mypid = getpid();
for (n = 0; n < argc; n++) {
s = argv[n];
/* Undocumented Solaris compat */
- if (!strncmp(s, "/proc/", 6)) {
+ if (strncmp(s, "/proc/", 6) == 0) {
s += 6;
}
errno = 0;
pid = strtol(s, &end, 10);
- if (pid < 0 || *end != '\0' || errno != 0) {
+ if (pid < 0 || pid > pid_max || *end != '\0' || errno != 0) {
warnx("%s: bad process id", s);
continue;
}
- if (pid == getpid()) {
+ if (pid == mypid) {
warnx("%s: skipping my own pid", s);
continue;
}
- for (i = 0; i < nleft; i++) {
- if (e[i].ident == (uintptr_t)pid) {
- break;
- }
+ if ((p = malloc(sizeof(*p))) == NULL) {
+ err(EX_OSERR, NULL);
}
- if (i < nleft) {
+ p->pid = pid;
+ if (RB_INSERT(pidtree, &pids, p) != NULL) {
/* Duplicate. */
+ free(p);
continue;
}
EV_SET(e + nleft, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL);
if (kevent(kq, e + nleft, 1, NULL, 0, NULL) == -1) {
+ if (errno != ESRCH)
+ err(EX_OSERR, "kevent()");
warn("%ld", pid);
- if (oflag) {
- exit(EX_OK);
- }
+ RB_REMOVE(pidtree, &pids, p);
+ free(p);
+ ndone++;
} else {
nleft++;
}
}
- if (nleft > 0 && tflag) {
+ if ((ndone == 0 || !oflag) && nleft > 0 && tflag) {
/*
* Explicitly detect SIGALRM so that an exit status of 124
* can be returned rather than 142.
@@ -182,7 +216,8 @@ main(int argc, char *argv[])
err(EX_OSERR, "setitimer");
}
}
- while (nleft > 0) {
+ ret = EX_OK;
+ while ((ndone == 0 || !oflag) && ret == EX_OK && nleft > 0) {
n = kevent(kq, NULL, 0, e, nleft + tflag, NULL);
if (n == -1) {
err(EX_OSERR, "kevent");
@@ -192,29 +227,34 @@ main(int argc, char *argv[])
if (verbose) {
printf("timeout\n");
}
- exit(124);
+ ret = 124;
}
+ pid = e[i].ident;
if (verbose) {
status = e[i].data;
if (WIFEXITED(status)) {
printf("%ld: exited with status %d.\n",
- (long)e[i].ident,
- WEXITSTATUS(status));
+ pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("%ld: killed by signal %d.\n",
- (long)e[i].ident,
- WTERMSIG(status));
+ pid, WTERMSIG(status));
} else {
- printf("%ld: terminated.\n",
- (long)e[i].ident);
+ printf("%ld: terminated.\n", pid);
}
}
- if (oflag) {
- exit(EX_OK);
+ k.pid = pid;
+ if ((p = RB_FIND(pidtree, &pids, &k)) != NULL) {
+ RB_REMOVE(pidtree, &pids, p);
+ free(p);
+ ndone++;
}
--nleft;
}
}
-
- exit(EX_OK);
+ if (pflag) {
+ RB_FOREACH(p, pidtree, &pids) {
+ printf("%d\n", p->pid);
+ }
+ }
+ exit(ret);
}
diff --git a/bin/pwait/tests/pwait_test.sh b/bin/pwait/tests/pwait_test.sh
index 66bdd6981704..d31ca21cff93 100644
--- a/bin/pwait/tests/pwait_test.sh
+++ b/bin/pwait/tests/pwait_test.sh
@@ -310,6 +310,43 @@ or_flag_cleanup()
wait $p2 $p4 $p6 >/dev/null 2>&1
}
+atf_test_case print
+print_head()
+{
+ atf_set "descr" "Test the -p flag"
+}
+
+print_body()
+{
+ sleep 1 &
+ p1=$!
+
+ sleep 5 &
+ p5=$!
+
+ sleep 10 &
+ p10=$!
+
+ atf_check \
+ -o inline:"$p5\n$p10\n" \
+ -s exit:124 \
+ pwait -t 2 -p $p10 $p5 $p1 $p5 $p10
+
+ atf_check \
+ -e inline:"kill: $p1: No such process\n" \
+ -s exit:1 \
+ kill -0 $p1
+
+ atf_check kill -0 $p5
+ atf_check kill -0 $p10
+}
+
+print_cleanup()
+{
+ kill $p1 $p5 $p10 >/dev/null 2>&1
+ wait $p1 $p5 $p10 >/dev/null 2>&1
+}
+
atf_init_test_cases()
{
atf_add_test_case basic
@@ -318,4 +355,5 @@ atf_init_test_cases()
atf_add_test_case timeout_no_timeout
atf_add_test_case timeout_many
atf_add_test_case or_flag
+ atf_add_test_case print
}
diff --git a/bin/rm/rm.c b/bin/rm/rm.c
index 16bbf7403fd4..2c41d7380cea 100644
--- a/bin/rm/rm.c
+++ b/bin/rm/rm.c
@@ -184,7 +184,7 @@ rm_tree(char **argv)
flags = FTS_PHYSICAL;
if (!needstat)
flags |= FTS_NOSTAT;
- if (Wflag)
+ if (Wflag || fflag)
flags |= FTS_WHITEOUT;
if (xflag)
flags |= FTS_XDEV;
@@ -273,7 +273,7 @@ rm_tree(char **argv)
case FTS_W:
rval = undelete(p->fts_accpath);
- if (rval == 0 && (fflag && errno == ENOENT)) {
+ if (rval == 0 || (fflag && errno == ENOENT)) {
if (vflag)
(void)printf("%s\n",
p->fts_path);
diff --git a/bin/sh/Makefile b/bin/sh/Makefile
index 916bb88b57fa..2b1eca8e4b31 100644
--- a/bin/sh/Makefile
+++ b/bin/sh/Makefile
@@ -69,11 +69,4 @@ token.h: mktokens
HAS_TESTS=
SUBDIR.${MK_TESTS}+= tests
-beforeinstallconfig:
- rm -f ${DESTDIR}/.profile
-
-LINKMODE=${CONFMODE}
-afterinstallconfig:
- ${INSTALL_LINK} ${TAG_ARGS:D${TAG_ARGS},config} ${DESTDIR}/root/.profile ${DESTDIR}/.profile
-
.include <bsd.prog.mk>
diff --git a/bin/sh/dot.profile b/bin/sh/dot.profile
index d27a2ae2fdbe..cba9bcf18ad9 100644
--- a/bin/sh/dot.profile
+++ b/bin/sh/dot.profile
@@ -1,6 +1,4 @@
#
-HOME=/root
-export HOME
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:~/bin
export PATH
TERM=${TERM:-xterm}
diff --git a/bin/sh/jobs.c b/bin/sh/jobs.c
index 1328ae50edef..a4cd76473921 100644
--- a/bin/sh/jobs.c
+++ b/bin/sh/jobs.c
@@ -573,6 +573,7 @@ waitcmdloop(struct job *job)
freejob(job);
else {
job->remembered = 0;
+ deljob(job);
if (job == bgjob)
bgjob = NULL;
}
@@ -599,7 +600,7 @@ waitcmdloop(struct job *job)
break;
}
}
- } while (dowait(DOWAIT_BLOCK | DOWAIT_SIG, (struct job *)NULL) != -1);
+ } while (dowait(DOWAIT_BLOCK | DOWAIT_SIG, job) != -1);
sig = pendingsig_waitcmd;
pendingsig_waitcmd = 0;
@@ -1077,6 +1078,7 @@ waitforjob(struct job *jp, int *signaled)
#if JOBS
int propagate_int = jp->jobctl && jp->foreground;
#endif
+ int jobindex;
int status;
int st;
@@ -1084,8 +1086,11 @@ waitforjob(struct job *jp, int *signaled)
TRACE(("waitforjob(%%%td) called\n", jp - jobtab + 1));
while (jp->state == 0)
if (dowait(DOWAIT_BLOCK | (Tflag ? DOWAIT_SIG |
- DOWAIT_SIG_TRAP : 0), jp) == -1)
+ DOWAIT_SIG_TRAP : 0), jp) == -1) {
+ jobindex = jp - jobtab;
dotrap();
+ jp = jobtab + jobindex;
+ }
#if JOBS
if (jp->jobctl) {
if (ttyfd >= 0 && tcsetpgrp(ttyfd, rootpid) < 0)
diff --git a/bin/sh/miscbltin.c b/bin/sh/miscbltin.c
index 9d0280bb548a..bbf0aa5b8bde 100644
--- a/bin/sh/miscbltin.c
+++ b/bin/sh/miscbltin.c
@@ -40,11 +40,14 @@
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/resource.h>
-#include <unistd.h>
+
#include <errno.h>
+#include <poll.h>
+#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
+#include <unistd.h>
#include "shell.h"
#include "options.h"
@@ -162,17 +165,18 @@ readcmd(int argc __unused, char **argv __unused)
int is_ifs;
int saveall = 0;
ptrdiff_t lastnonifs, lastnonifsws;
- struct timeval tv;
- char *tvptr;
- fd_set ifds;
+ sigset_t set, oset;
+ intmax_t number, timeout;
+ struct timespec tnow, tend, tresid;
+ struct pollfd pfd;
+ char *endptr;
ssize_t nread;
int sig;
struct fdctx fdctx;
rflag = 0;
prompt = NULL;
- tv.tv_sec = -1;
- tv.tv_usec = 0;
+ timeout = -1;
while ((i = nextopt("erp:t:")) != '\0') {
switch(i) {
case 'p':
@@ -184,22 +188,29 @@ readcmd(int argc __unused, char **argv __unused)
rflag = 1;
break;
case 't':
- tv.tv_sec = strtol(shoptarg, &tvptr, 0);
- if (tvptr == shoptarg)
- error("timeout value");
- switch(*tvptr) {
- case 0:
- case 's':
- break;
- case 'h':
- tv.tv_sec *= 60;
- /* FALLTHROUGH */
- case 'm':
- tv.tv_sec *= 60;
- break;
- default:
- error("timeout unit");
- }
+ timeout = 0;
+ do {
+ number = strtol(shoptarg, &endptr, 0);
+ if (number < 0 || endptr == shoptarg)
+ error("timeout value");
+ switch (*endptr) {
+ case 's':
+ endptr++;
+ break;
+ case 'h':
+ number *= 60;
+ /* FALLTHROUGH */
+ case 'm':
+ number *= 60;
+ endptr++;
+ break;
+ }
+ if (*endptr != '\0' &&
+ !(*endptr >= '0' && *endptr <= '9'))
+ error("timeout unit");
+ timeout += number;
+ shoptarg = endptr;
+ } while (*shoptarg != '\0');
break;
}
}
@@ -212,13 +223,33 @@ readcmd(int argc __unused, char **argv __unused)
if ((ifs = bltinlookup("IFS", 1)) == NULL)
ifs = " \t\n";
- if (tv.tv_sec >= 0) {
+ if (timeout >= 0) {
/*
* Wait for something to become available.
*/
- FD_ZERO(&ifds);
- FD_SET(0, &ifds);
- status = select(1, &ifds, NULL, NULL, &tv);
+ pfd.fd = STDIN_FILENO;
+ pfd.events = POLLIN;
+ status = sig = 0;
+ sigfillset(&set);
+ sigprocmask(SIG_SETMASK, &set, &oset);
+ if (pendingsig) {
+ /* caught a signal already */
+ status = -1;
+ } else if (timeout == 0) {
+ status = poll(&pfd, 1, 0);
+ } else {
+ clock_gettime(CLOCK_UPTIME, &tnow);
+ tend = tnow;
+ tend.tv_sec += timeout;
+ do {
+ timespecsub(&tend, &tnow, &tresid);
+ status = ppoll(&pfd, 1, &tresid, &oset);
+ if (status >= 0 || pendingsig != 0)
+ break;
+ clock_gettime(CLOCK_UPTIME, &tnow);
+ } while (timespeccmp(&tnow, &tend, <));
+ }
+ sigprocmask(SIG_SETMASK, &oset, NULL);
/*
* If there's nothing ready, return an error.
*/
diff --git a/bin/sh/parser.c b/bin/sh/parser.c
index 0c1b7a91c257..3e42d41caec4 100644
--- a/bin/sh/parser.c
+++ b/bin/sh/parser.c
@@ -55,6 +55,8 @@
#include "show.h"
#include "eval.h"
#include "exec.h" /* to check for special builtins */
+#include "main.h"
+#include "jobs.h"
#ifndef NO_HISTORY
#include "myhistedit.h"
#endif
@@ -2050,7 +2052,129 @@ getprompt(void *unused __unused)
* Format prompt string.
*/
for (i = 0; (i < PROMPTLEN - 1) && (*fmt != '\0'); i++, fmt++) {
- if (*fmt != '\\') {
+ if (*fmt == '$') {
+ const char *varname_start, *varname_end, *value;
+ char varname[256];
+ int namelen, braced = 0;
+
+ fmt++; /* Skip the '$' */
+
+ /* Check for ${VAR} syntax */
+ if (*fmt == '{') {
+ braced = 1;
+ fmt++;
+ }
+
+ varname_start = fmt;
+
+ /* Extract variable name */
+ if (is_digit(*fmt)) {
+ /* Positional parameter: $0, $1, etc. */
+ fmt++;
+ varname_end = fmt;
+ } else if (is_special(*fmt)) {
+ /* Special parameter: $?, $!, $$, etc. */
+ fmt++;
+ varname_end = fmt;
+ } else if (is_name(*fmt)) {
+ /* Regular variable name */
+ do
+ fmt++;
+ while (is_in_name(*fmt));
+ varname_end = fmt;
+ } else {
+ /*
+ * Not a valid variable reference.
+ * Output literal '$'.
+ */
+ ps[i] = '$';
+ if (braced && i < PROMPTLEN - 2)
+ ps[++i] = '{';
+ fmt = varname_start - 1;
+ continue;
+ }
+
+ namelen = varname_end - varname_start;
+ if (namelen == 0 || namelen >= (int)sizeof(varname)) {
+ /* Invalid or too long, output literal */
+ ps[i] = '$';
+ fmt = varname_start - 1;
+ continue;
+ }
+
+ /* Copy variable name */
+ memcpy(varname, varname_start, namelen);
+ varname[namelen] = '\0';
+
+ /* Handle closing brace for ${VAR} */
+ if (braced) {
+ if (*fmt == '}') {
+ fmt++;
+ } else {
+ /* Missing closing brace, treat as literal */
+ ps[i] = '$';
+ if (i < PROMPTLEN - 2)
+ ps[++i] = '{';
+ fmt = varname_start - 1;
+ continue;
+ }
+ }
+
+ /* Look up the variable */
+ if (namelen == 1 && is_digit(*varname)) {
+ /* Positional parameters - check digits FIRST */
+ int num = *varname - '0';
+ if (num == 0)
+ value = arg0 ? arg0 : "";
+ else if (num > 0 && num <= shellparam.nparam)
+ value = shellparam.p[num - 1];
+ else
+ value = "";
+ } else if (namelen == 1 && is_special(*varname)) {
+ /* Special parameters */
+ char valbuf[20];
+ int num;
+
+ switch (*varname) {
+ case '$':
+ num = rootpid;
+ break;
+ case '?':
+ num = exitstatus;
+ break;
+ case '#':
+ num = shellparam.nparam;
+ break;
+ case '!':
+ num = backgndpidval();
+ break;
+ default:
+ num = 0;
+ break;
+ }
+ snprintf(valbuf, sizeof(valbuf), "%d", num);
+ value = valbuf;
+ } else {
+ /* Regular variables */
+ value = lookupvar(varname);
+ if (value == NULL)
+ value = "";
+ }
+
+ /* Copy value to output, respecting buffer size */
+ while (*value != '\0' && i < PROMPTLEN - 1) {
+ ps[i++] = *value++;
+ }
+
+ /*
+ * Adjust fmt and i for the loop increment.
+ * fmt will be incremented by the for loop,
+ * so position it one before where we want.
+ */
+ fmt--;
+ i--;
+ continue;
+ } else if (*fmt != '\\') {
ps[i] = *fmt;
continue;
}
diff --git a/bin/sh/sh.1 b/bin/sh/sh.1
index 7ef22fa352bb..affb653cd3ae 100644
--- a/bin/sh/sh.1
+++ b/bin/sh/sh.1
@@ -31,7 +31,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd May 1, 2025
+.Dd November 17, 2025
.Dt SH 1
.Os
.Sh NAME
@@ -2544,6 +2544,10 @@ to explicitly specify seconds, minutes or hours.
If none is supplied,
.Ql s
is assumed.
+Multiple value-unit groups may be stringed together, in which case
+they are added up, e.g.\&
+.Ql 1h30m15s
+which adds up to 5,415 seconds.
.Pp
The
.Fl e
diff --git a/bin/sh/tests/builtins/Makefile b/bin/sh/tests/builtins/Makefile
index 7fdecb23c817..0246009cce81 100644
--- a/bin/sh/tests/builtins/Makefile
+++ b/bin/sh/tests/builtins/Makefile
@@ -143,6 +143,7 @@ ${PACKAGE}FILES+= read8.0
${PACKAGE}FILES+= read9.0
${PACKAGE}FILES+= read10.0
${PACKAGE}FILES+= read11.0
+${PACKAGE}FILES+= read12.0
${PACKAGE}FILES+= return1.0
${PACKAGE}FILES+= return2.1
${PACKAGE}FILES+= return3.1
@@ -188,5 +189,6 @@ ${PACKAGE}FILES+= wait7.0
${PACKAGE}FILES+= wait8.0
${PACKAGE}FILES+= wait9.127
${PACKAGE}FILES+= wait10.0
+${PACKAGE}FILES+= wait11.0
.include <bsd.test.mk>
diff --git a/bin/sh/tests/builtins/read11.0 b/bin/sh/tests/builtins/read11.0
index c75ed9c92a83..07bd3e70644c 100644
--- a/bin/sh/tests/builtins/read11.0
+++ b/bin/sh/tests/builtins/read11.0
@@ -1,3 +1,5 @@
+# Verify that `read -t 0 v` succeeds immediately if input is available
+# and fails immediately if not
set -e
@@ -5,13 +7,26 @@ T=$(mktemp -d ${TMPDIR:-/tmp}/sh-test.XXXXXX)
trap 'rm -rf "$T"' 0
cd $T
mkfifo fifo1
-# Open fifo1 for writing and then read block on a dummy fifo
-{ mkfifo fifo2; read dummy <fifo2; } >fifo1 &
+# Open fifo1 for writing
+{ echo new_value; sleep 10; } >fifo1 &
# Wait for the child to open fifo1 for writing
exec 3<fifo1
+
+v=original_value
+r=0
+ts=$(date +%s%3N)
+read -t 0 v <&3 || r=$?
+te=$(date +%s%3N)
+[ "$r" -eq 0 ]
+[ $((te-ts)) -lt 250 ]
+[ "$v" = "new_value" ]
+
v=original_value
r=0
+ts=$(date +%s%3N)
read -t 0 v <&3 || r=$?
+te=$(date +%s%3N)
kill -TERM "$!" || :
-{ [ "$r" -gt 128 ] && [ "$(kill -l "$r")" = ALRM ]; } || exit
+[ "$r" -gt 128 ] && [ "$(kill -l "$r")" = ALRM ]
+[ $((te-ts)) -lt 250 ]
[ -z "$v" ]
diff --git a/bin/sh/tests/builtins/read12.0 b/bin/sh/tests/builtins/read12.0
new file mode 100644
index 000000000000..4551555adfed
--- /dev/null
+++ b/bin/sh/tests/builtins/read12.0
@@ -0,0 +1,32 @@
+# Verify that `read -t 3 v` succeeds immediately if input is available
+# and times out after 3 s if not
+
+set -e
+
+T=$(mktemp -d ${TMPDIR:-/tmp}/sh-test.XXXXXX)
+trap 'rm -rf "$T"' 0
+cd $T
+mkfifo fifo1
+# Open fifo1 for writing
+{ echo new_value; sleep 10; } >fifo1 &
+# Wait for the child to open fifo1 for writing
+exec 3<fifo1
+
+v=original_value
+r=0
+ts=$(date +%s%3N)
+read -t 3 v <&3 || r=$?
+te=$(date +%s%3N)
+[ "$r" -eq 0 ]
+[ $((te-ts)) -lt 250 ]
+[ "$v" = "new_value" ]
+
+v=original_value
+r=0
+ts=$(date +%s%3N)
+read -t 3 v <&3 || r=$?
+te=$(date +%s%3N)
+kill -TERM "$!" || :
+[ "$r" -gt 128 ] && [ "$(kill -l "$r")" = ALRM ]
+[ $((te-ts)) -gt 3000 ] && [ $((te-ts)) -lt 3250 ]
+[ -z "$v" ]
diff --git a/bin/sh/tests/builtins/wait11.0 b/bin/sh/tests/builtins/wait11.0
new file mode 100644
index 000000000000..d5fab26fb677
--- /dev/null
+++ b/bin/sh/tests/builtins/wait11.0
@@ -0,0 +1,6 @@
+sleep 3 | sleep 2 &
+sleep 3 &
+kill %1
+wait %1
+r=$?
+[ "$r" -gt 128 ] && [ "$(kill -l "$r")" = TERM ]
diff --git a/bin/sh/tests/execution/Makefile b/bin/sh/tests/execution/Makefile
index 53cb97db9393..dde562a082cd 100644
--- a/bin/sh/tests/execution/Makefile
+++ b/bin/sh/tests/execution/Makefile
@@ -18,6 +18,7 @@ ${PACKAGE}FILES+= bg10.0 bg10.0.stdout
${PACKAGE}FILES+= bg11.0
${PACKAGE}FILES+= bg12.0
${PACKAGE}FILES+= bg13.0
+${PACKAGE}FILES+= bg14.0
${PACKAGE}FILES+= env1.0
${PACKAGE}FILES+= fork1.0
${PACKAGE}FILES+= fork2.0
diff --git a/bin/sh/tests/execution/bg14.0 b/bin/sh/tests/execution/bg14.0
new file mode 100644
index 000000000000..e27f77e9b7b3
--- /dev/null
+++ b/bin/sh/tests/execution/bg14.0
@@ -0,0 +1,9 @@
+T=`mktemp -d ${TMPDIR:-/tmp}/sh-test.XXXXXXXX`
+trap 'rm -rf "$T"' 0
+cd "$T" || exit 3
+mkfifo fifo1 || exit 3
+set -T
+trap "for i in 1 2 3 4; do sleep 1 & done" USR1
+sleep 1 &
+{ kill -USR1 "$$"; echo .; } >fifo1 &
+(read dummy <fifo1)
diff --git a/bin/sh/tests/parser/Makefile b/bin/sh/tests/parser/Makefile
index afeb604710e4..c22af5414526 100644
--- a/bin/sh/tests/parser/Makefile
+++ b/bin/sh/tests/parser/Makefile
@@ -86,6 +86,12 @@ ${PACKAGE}FILES+= only-redir2.0
${PACKAGE}FILES+= only-redir3.0
${PACKAGE}FILES+= only-redir4.0
${PACKAGE}FILES+= pipe-not1.0
+${PACKAGE}FILES+= ps1-expand1.0
+${PACKAGE}FILES+= ps1-expand2.0
+${PACKAGE}FILES+= ps1-expand3.0
+${PACKAGE}FILES+= ps1-expand4.0
+${PACKAGE}FILES+= ps1-expand5.0
+${PACKAGE}FILES+= ps2-expand1.0
${PACKAGE}FILES+= set-v1.0 set-v1.0.stderr
${PACKAGE}FILES+= var-assign1.0
diff --git a/bin/sh/tests/parser/ps1-expand1.0 b/bin/sh/tests/parser/ps1-expand1.0
new file mode 100644
index 000000000000..351e6437a023
--- /dev/null
+++ b/bin/sh/tests/parser/ps1-expand1.0
@@ -0,0 +1,7 @@
+# Test simple variable expansion in PS1
+testvar=abcdef
+output=$(testvar=abcdef PS1='$testvar:' ENV=/dev/null ${SH} +m -i </dev/null 2>&1)
+case $output in
+*abcdef*) exit 0 ;;
+*) echo "Expected 'abcdef' in prompt output"; exit 1 ;;
+esac
diff --git a/bin/sh/tests/parser/ps1-expand2.0 b/bin/sh/tests/parser/ps1-expand2.0
new file mode 100644
index 000000000000..ed31a7c17136
--- /dev/null
+++ b/bin/sh/tests/parser/ps1-expand2.0
@@ -0,0 +1,7 @@
+# Test braced variable expansion in PS1
+testvar=xyz123
+output=$(testvar=xyz123 PS1='prefix-${testvar}-suffix:' ENV=/dev/null ${SH} +m -i </dev/null 2>&1)
+case $output in
+*xyz123*) exit 0 ;;
+*) echo "Expected 'xyz123' in prompt output"; exit 1 ;;
+esac
diff --git a/bin/sh/tests/parser/ps1-expand3.0 b/bin/sh/tests/parser/ps1-expand3.0
new file mode 100644
index 000000000000..0b6270c300ff
--- /dev/null
+++ b/bin/sh/tests/parser/ps1-expand3.0
@@ -0,0 +1,8 @@
+# Test special parameter $$ (PID) in PS1
+output=$(PS1='pid:$$:' ENV=/dev/null ${SH} +m -i </dev/null 2>&1)
+# Check that output contains "pid:" followed by a number (not literal $$)
+case $output in
+*pid:\$\$:*) echo "PID not expanded, got literal \$\$"; exit 1 ;;
+*pid:[0-9]*) exit 0 ;;
+*) echo "Expected PID after 'pid:' in output"; exit 1 ;;
+esac
diff --git a/bin/sh/tests/parser/ps1-expand4.0 b/bin/sh/tests/parser/ps1-expand4.0
new file mode 100644
index 000000000000..623c52707eec
--- /dev/null
+++ b/bin/sh/tests/parser/ps1-expand4.0
@@ -0,0 +1,8 @@
+# Test special parameter $? (exit status) in PS1
+output=$(PS1='status:$?:' ENV=/dev/null ${SH} +m -i </dev/null 2>&1)
+# Should start with exit status 0
+case $output in
+*status:\$?:*) echo "Exit status not expanded, got literal \$?"; exit 1 ;;
+*status:0:*) exit 0 ;;
+*) echo "Expected 'status:0:' in initial prompt"; exit 1 ;;
+esac
diff --git a/bin/sh/tests/parser/ps1-expand5.0 b/bin/sh/tests/parser/ps1-expand5.0
new file mode 100644
index 000000000000..73fe3ba5a3d5
--- /dev/null
+++ b/bin/sh/tests/parser/ps1-expand5.0
@@ -0,0 +1,8 @@
+# Test positional parameter $0 in PS1
+output=$(PS1='shell:$0:' ENV=/dev/null ${SH} +m -i </dev/null 2>&1)
+# $0 should contain the shell name/path
+case $output in
+*shell:\$0:*) echo "Positional parameter not expanded, got literal \$0"; exit 1 ;;
+*shell:*sh*:*) exit 0 ;;
+*) echo "Expected shell name after 'shell:' in output"; exit 1 ;;
+esac
diff --git a/bin/sh/tests/parser/ps2-expand1.0 b/bin/sh/tests/parser/ps2-expand1.0
new file mode 100644
index 000000000000..f0a3a77ded1c
--- /dev/null
+++ b/bin/sh/tests/parser/ps2-expand1.0
@@ -0,0 +1,12 @@
+# Test variable expansion in PS2 (continuation prompt)
+testvar=continue
+# Send incomplete command (backslash at end) to trigger PS2
+output=$(testvar=continue PS2='$testvar>' ENV=/dev/null ${SH} +m -i <<EOF 2>&1
+echo \\
+done
+EOF
+)
+case $output in
+*continue\>*) exit 0 ;;
+*) echo "Expected 'continue>' in PS2 output"; exit 1 ;;
+esac