summaryrefslogtreecommitdiff
path: root/corebinutils
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:27:51 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:27:51 +0300
commit76166d4f612e1318ffe99493f9d8b578887b1278 (patch)
tree3bcce2f92ac70b9f282c905397f4bdc32dc70b39 /corebinutils
parente99e87d16cdba1de0cfd84ce1a231ad3371e43db (diff)
parent29b985290c171926c765c571e8711276c4d9aae0 (diff)
downloadProject-Tick-76166d4f612e1318ffe99493f9d8b578887b1278.tar.gz
Project-Tick-76166d4f612e1318ffe99493f9d8b578887b1278.zip
Add 'corebinutils/ps/' from commit '29b985290c171926c765c571e8711276c4d9aae0'
git-subtree-dir: corebinutils/ps git-subtree-mainline: e99e87d16cdba1de0cfd84ce1a231ad3371e43db git-subtree-split: 29b985290c171926c765c571e8711276c4d9aae0
Diffstat (limited to 'corebinutils')
-rw-r--r--corebinutils/ps/.gitignore3
-rw-r--r--corebinutils/ps/GNUmakefile35
-rw-r--r--corebinutils/ps/LICENSE30
-rw-r--r--corebinutils/ps/LICENSES/BSD-3-Clause.txt11
-rw-r--r--corebinutils/ps/README.md71
-rw-r--r--corebinutils/ps/extern.h124
-rw-r--r--corebinutils/ps/fmt.c48
-rw-r--r--corebinutils/ps/keyword.c249
-rw-r--r--corebinutils/ps/nlist.c76
-rw-r--r--corebinutils/ps/print.c596
-rw-r--r--corebinutils/ps/ps.11020
-rw-r--r--corebinutils/ps/ps.c638
-rw-r--r--corebinutils/ps/ps.h205
-rw-r--r--corebinutils/ps/tests/spin_helper.c45
-rw-r--r--corebinutils/ps/tests/test.sh150
15 files changed, 3301 insertions, 0 deletions
diff --git a/corebinutils/ps/.gitignore b/corebinutils/ps/.gitignore
new file mode 100644
index 0000000000..3193332093
--- /dev/null
+++ b/corebinutils/ps/.gitignore
@@ -0,0 +1,3 @@
+build/
+out/
+*.o
diff --git a/corebinutils/ps/GNUmakefile b/corebinutils/ps/GNUmakefile
new file mode 100644
index 0000000000..eb27646501
--- /dev/null
+++ b/corebinutils/ps/GNUmakefile
@@ -0,0 +1,35 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS ?=
+CPPFLAGS += -D_GNU_SOURCE
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror -Wno-unused-parameter
+LDFLAGS ?=
+LDLIBS ?= -lm
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/ps
+
+SRCS := ps.c keyword.c print.c fmt.c nlist.c
+OBJS := $(SRCS:%.c=$(OBJDIR)/%.o)
+
+.PHONY: all clean dirs test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/%.o: $(CURDIR)/%.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$<" -o "$@"
+
+test: $(TARGET)
+ CC="$(CC)" PS_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/ps/LICENSE b/corebinutils/ps/LICENSE
new file mode 100644
index 0000000000..7b54c296a7
--- /dev/null
+++ b/corebinutils/ps/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 1990, 1993, 1994 The Regents of the University of California. All rights reserved.
+Copyright (c) 2025 The FreeBSD Foundation
+Copyright (c) 2026 Project Tick. All rights reserved.
+
+This code is derived from software contributed to Berkeley by
+Ken Smith of The State University of New York at Buffalo.
+
+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/ps/LICENSES/BSD-3-Clause.txt b/corebinutils/ps/LICENSES/BSD-3-Clause.txt
new file mode 100644
index 0000000000..ea890afbc7
--- /dev/null
+++ b/corebinutils/ps/LICENSES/BSD-3-Clause.txt
@@ -0,0 +1,11 @@
+Copyright (c) <year> <owner>.
+
+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 copyright holder 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 COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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/ps/README.md b/corebinutils/ps/README.md
new file mode 100644
index 0000000000..4d69c1bc68
--- /dev/null
+++ b/corebinutils/ps/README.md
@@ -0,0 +1,71 @@
+# ps
+
+Standalone Linux-native port of FreeBSD `ps` 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
+
+| BSD mechanism | Linux replacement |
+|---|---|
+| `kvm_openfiles(3)` / `kvm_getprocs(3)` | `/proc` directory enumeration |
+| `struct kinfo_proc` | `/proc/[pid]/stat` + `/proc/[pid]/status` + `/proc/[pid]/cmdline` + `/proc/[pid]/wchan` |
+| `sysctl(3)` / `nlist(3)` | `/proc/meminfo` (for physical memory stats) |
+| `libxo` | Standard `printf` output (libxo not available on target Linux-musl) |
+| `vis(3)` / `strvis(3)` | Simplified visual encoding for non-printable characters |
+| `SLIST_*` / `STAILQ_*` (BSD queue macros) | Kept or replaced with standard C logic |
+
+### /proc field mapping
+
+- `pid`, `ppid`, `pgrp`, `sid`, `tdev`: parsed from `/proc/[pid]/stat` fields
+- `ruid`, `euid`, `rgid`, `egid`: parsed from `/proc/[pid]/status` (`Uid:`/`Gid:` lines)
+- `cpu-time`: `utime` + `stime` from `/proc/[pid]/stat` (fields 14 and 15)
+- `percent-cpu`: calculated from `cpu-time` and process uptime (from jiffies)
+- `virtual-size`: field 23 of `/proc/[pid]/stat` (in bytes)
+- `rss`: field 24 of `/proc/[pid]/stat` (in pages) * pagesize
+- `state`: field 3 of `/proc/[pid]/stat` (mapped from Linux to BSD-like characters)
+- `wchan`: read from `/proc/[pid]/wchan`
+
+## Supported Semantics (subset)
+
+- Default output: `pid,tt,state,time,command`
+- Common formats: `-j`, `-l`, `-u`, `-v` (BSD-style formats)
+- Selection: `-A` (all), `-a` (all with tty), `-p pid`, `-t tty`, `-U uid`, `-G gid`
+- Sorting: `-m` (by memory), `-r` (by CPU)
+- Output control: `-O`, `-o`, `-w` (width control), `-h` (repeat headers)
+
+## Unsupported / Not Available on Linux
+
+| Option | Reason |
+|---|---|
+| `-j jail` | FreeBSD jail IDs have no Linux equivalent |
+| `-class class` | FreeBSD login class has no Linux equivalent |
+| `-M core` | Requires `kvm(3)` for core file analysis |
+| `-N system` | Requires `kvm(3)` for name list extraction |
+| `-Z` | FreeBSD MAC labels don't map to a portable Linux interface |
+
+## Known Differences
+
+- `state` flags: Mapping is approximate (e.g., BSD `P_CONTROLT` has no exact flag in Linux `/proc/[pid]/stat`).
+- Memory stats: `tsiz`, `dsiz`, `ssiz` are hard to extract precisely on Linux via `/proc` compared to `struct kinfo_proc`. `vsz` and `rss` are accurate.
+- CPU % calculation: BSD-style decaying average (`ki_pctcpu`) is hard to replicate exactly; a life-time average is used instead.
+
+## Test Categories
+
+- CLI parsing and usage
+- Process selection: `-p`, `-A`, `-t`
+- Format selection: `-j`, `-l`, `-u`, `-o`
+- Sorting: `-m`, `-r`
+- Negative tests: invalid flags, non-existent pids.
diff --git a/corebinutils/ps/extern.h b/corebinutils/ps/extern.h
new file mode 100644
index 0000000000..b0b18240da
--- /dev/null
+++ b/corebinutils/ps/extern.h
@@ -0,0 +1,124 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1991, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * 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.
+ *
+ * Linux-native port: replaces kvm(3) prototypes with Linux /proc equivalents.
+ */
+
+#ifndef _EXTERN_H_
+#define _EXTERN_H_
+
+#ifndef nitems
+#define nitems(x) (sizeof(x) / sizeof((x)[0]))
+#endif
+
+#ifdef __cplusplus
+#define __BEGIN_DECLS extern "C" {
+#define __END_DECLS }
+#else
+#define __BEGIN_DECLS
+#define __END_DECLS
+#endif
+
+struct kinfo;
+struct var;
+struct varent;
+typedef struct kinfo KINFO;
+typedef struct varent VARENT;
+
+extern fixpt_t ccpu;
+extern int cflag, eval, fscale, nlistread, rawcpu;
+extern unsigned long mem_total_kb;
+extern time_t now;
+extern int showthreads, sumrusage, termwidth;
+extern struct velisthead varlist;
+extern const size_t known_keywords_nb;
+
+__BEGIN_DECLS
+char *arguments(KINFO *, VARENT *);
+void check_keywords(void);
+char *command(KINFO *, VARENT *);
+char *cputime(KINFO *, VARENT *);
+char *cpunum(KINFO *, VARENT *);
+int donlist(void);
+char *elapsed(KINFO *, VARENT *);
+char *elapseds(KINFO *, VARENT *);
+char *emulname(KINFO *, VARENT *);
+VARENT *find_varentry(const char *);
+char *fmt_argv(char **, char *, char *, size_t);
+double getpcpu(const KINFO *);
+char *jailname(KINFO *, VARENT *);
+size_t aliased_keyword_index(const VAR *);
+char *kvar(KINFO *, VARENT *);
+char *label(KINFO *, VARENT *);
+char *loginclass(KINFO *, VARENT *);
+char *logname(KINFO *, VARENT *);
+char *longtname(KINFO *, VARENT *);
+char *lstarted(KINFO *, VARENT *);
+char *maxrss(KINFO *, VARENT *);
+char *lockname(KINFO *, VARENT *);
+char *mwchan(KINFO *, VARENT *);
+char *nwchan(KINFO *, VARENT *);
+char *pagein(KINFO *, VARENT *);
+void parsefmt(const char *, struct velisthead *, int);
+char *pcpu(KINFO *, VARENT *);
+char *pmem(KINFO *, VARENT *);
+char *pri(KINFO *, VARENT *);
+void printheader(void);
+char *priorityr(KINFO *, VARENT *);
+char *egroupname(KINFO *, VARENT *);
+char *rgroupname(KINFO *, VARENT *);
+void resolve_aliases(void);
+char *runame(KINFO *, VARENT *);
+char *rvar(KINFO *, VARENT *);
+void showkey(void);
+char *started(KINFO *, VARENT *);
+char *state(KINFO *, VARENT *);
+char *systime(KINFO *, VARENT *);
+char *tdev(KINFO *, VARENT *);
+char *tdnam(KINFO *, VARENT *);
+char *tname(KINFO *, VARENT *);
+char *ucomm(KINFO *, VARENT *);
+char *username(KINFO *, VARENT *);
+char *upr(KINFO *, VARENT *);
+char *usertime(KINFO *, VARENT *);
+char *vsize(KINFO *, VARENT *);
+char *wchan(KINFO *, VARENT *);
+
+/* Linux only: user_from_uid and group_from_gid helpers if not in system */
+char *user_from_uid(uid_t, int);
+char *group_from_gid(gid_t, int);
+/* BSD-like devname(3) or equivalent */
+char *devname(dev_t, mode_t);
+void free_devnames(void);
+
+__END_DECLS
+
+#endif /* _EXTERN_H_ */
diff --git a/corebinutils/ps/fmt.c b/corebinutils/ps/fmt.c
new file mode 100644
index 0000000000..83fb445fd2
--- /dev/null
+++ b/corebinutils/ps/fmt.c
@@ -0,0 +1,48 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1992, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * 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.
+ */
+
+#include <sys/types.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <err.h>
+#include "ps.h"
+
+/* Simplified fmt_argv for Linux */
+char *
+fmt_argv(char **argv, char *cmd, char *thread, size_t maxlen)
+{
+ char *res = strdup(cmd ? cmd : "");
+ if (!res)
+ err(1, "malloc failed");
+ return res;
+}
diff --git a/corebinutils/ps/keyword.c b/corebinutils/ps/keyword.c
new file mode 100644
index 0000000000..e53771201b
--- /dev/null
+++ b/corebinutils/ps/keyword.c
@@ -0,0 +1,249 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1990, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * Copyright (c) 2025 The FreeBSD Foundation
+ * Copyright (c) 2026 Project Tick. All rights reserved.
+ *
+ * Portions of this software were developed by Olivier Certner
+ * <olce@FreeBSD.org> at Kumacom SARL under sponsorship from the FreeBSD
+ * Foundation.
+ *
+ * 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/param.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <err.h>
+
+#include "ps.h"
+
+static int vcmp(const void *, const void *);
+
+#define KOFF(x) offsetof(struct kinfo_proc, x)
+#define ROFF(x) offsetof(struct rusage, x)
+
+#define UIDFMT "u"
+#define PIDFMT "d"
+
+/* Sorted alphabetically by name */
+static VAR keywords[] = {
+ {"%cpu", {NULL}, "%CPU", NULL, 0, pcpu, 0, UNSPEC, NULL},
+ {"%mem", {NULL}, "%MEM", NULL, 0, pmem, 0, UNSPEC, NULL},
+ {"args", {NULL}, "COMMAND", NULL, COMM|LJUST|USER, arguments, 0, UNSPEC, NULL},
+ {"comm", {NULL}, "COMMAND", NULL, LJUST, ucomm, 0, UNSPEC, NULL},
+ {"command", {NULL}, "COMMAND", NULL, COMM|LJUST|USER, command, 0, UNSPEC, NULL},
+ {"cpu", {NULL}, "C", NULL, 0, cpunum, 0, UNSPEC, NULL},
+ {"cputime", {"time"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"dsiz", {NULL}, "DSIZ", NULL, 0, kvar, KOFF(ki_dsize), PGTOK, "ld"},
+ {"egid", {"gid"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"egroup", {"group"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"emul", {NULL}, "EMUL", NULL, LJUST, emulname, 0, UNSPEC, NULL},
+ {"etime", {NULL}, "ELAPSED", NULL, USER, elapsed, 0, UNSPEC, NULL},
+ {"etimes", {NULL}, "ELAPSED", NULL, USER, elapseds, 0, UNSPEC, NULL},
+ {"euid", {"uid"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"f", {NULL}, "F", NULL, 0, kvar, KOFF(ki_flag), LONG, "lx"},
+ {"flags", {"f"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"gid", {NULL}, "GID", NULL, 0, kvar, KOFF(ki_groups), UINT, UIDFMT},
+ {"group", {NULL}, "GROUP", NULL, LJUST, egroupname, 0, UNSPEC, NULL},
+ {"jail", {NULL}, "JAIL", NULL, LJUST, jailname, 0, UNSPEC, NULL},
+ {"jid", {NULL}, "JID", NULL, 0, kvar, KOFF(ki_jid), INT, "d"},
+ {"jobc", {NULL}, "JOBC", NULL, 0, kvar, KOFF(ki_sid), INT, "d"}, /* session as jobc proxy */
+ {"label", {NULL}, "LABEL", NULL, LJUST, label, 0, UNSPEC, NULL},
+ {"lim", {NULL}, "LIM", NULL, 0, maxrss, 0, UNSPEC, NULL},
+ {"login", {NULL}, "LOGIN", NULL, LJUST, logname, 0, UNSPEC, NULL},
+ {"logname", {"login"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"lstart", {NULL}, "STARTED", NULL, LJUST|USER, lstarted, 0, UNSPEC, NULL},
+ {"lwp", {NULL}, "LWP", NULL, 0, kvar, KOFF(ki_pid), UINT, PIDFMT},
+ {"mwchan", {NULL}, "MWCHAN", NULL, LJUST, mwchan, 0, UNSPEC, NULL},
+ {"ni", {"nice"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"nice", {NULL}, "NI", NULL, 0, kvar, KOFF(ki_nice), CHAR, "d"},
+ {"nlwp", {NULL}, "NLWP", NULL, 0, kvar, KOFF(ki_numthreads), UINT, "d"},
+ {"nwchan", {NULL}, "NWCHAN", NULL, LJUST, nwchan, 0, UNSPEC, NULL},
+ {"pagein", {NULL}, "PAGEIN", NULL, USER, pagein, 0, UNSPEC, NULL},
+ {"pcpu", {"%cpu"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"pgid", {NULL}, "PGID", NULL, 0, kvar, KOFF(ki_pgid), UINT, PIDFMT},
+ {"pid", {NULL}, "PID", NULL, 0, kvar, KOFF(ki_pid), UINT, PIDFMT},
+ {"pmem", {"%mem"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"ppid", {NULL}, "PPID", NULL, 0, kvar, KOFF(ki_ppid), UINT, PIDFMT},
+ {"pri", {NULL}, "PRI", NULL, 0, pri, 0, UNSPEC, NULL},
+ {"rgid", {NULL}, "RGID", NULL, 0, kvar, KOFF(ki_rgid), UINT, UIDFMT},
+ {"rgroup", {NULL}, "RGROUP", NULL, LJUST, rgroupname, 0, UNSPEC, NULL},
+ {"rss", {NULL}, "RSS", NULL, 0, kvar, KOFF(ki_rssize), PGTOK, "ld"},
+ {"ruid", {NULL}, "RUID", NULL, 0, kvar, KOFF(ki_ruid), UINT, UIDFMT},
+ {"ruser", {NULL}, "RUSER", NULL, LJUST, runame, 0, UNSPEC, NULL},
+ {"sid", {NULL}, "SID", NULL, 0, kvar, KOFF(ki_sid), UINT, PIDFMT},
+ {"sl", {NULL}, "SL", NULL, INF127, kvar, KOFF(ki_slptime), UINT, "d"},
+ {"ssiz", {NULL}, "SSIZ", NULL, 0, kvar, KOFF(ki_ssize), PGTOK, "ld"},
+ {"start", {NULL}, "STARTED", NULL, LJUST|USER, started, 0, UNSPEC, NULL},
+ {"stat", {"state"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"state", {NULL}, "STAT", NULL, LJUST, state, 0, UNSPEC, NULL},
+ {"svgid", {NULL}, "SVGID", NULL, 0, kvar, KOFF(ki_svgid), UINT, UIDFMT},
+ {"svuid", {NULL}, "SVUID", NULL, 0, kvar, KOFF(ki_svuid), UINT, UIDFMT},
+ {"systime", {NULL}, "SYSTIME", NULL, USER, systime, 0, UNSPEC, NULL},
+ {"tdev", {NULL}, "TDEV", NULL, 0, tdev, 0, UNSPEC, NULL},
+ {"tid", {"lwp"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"time", {NULL}, "TIME", NULL, USER, cputime, 0, UNSPEC, NULL},
+ {"tpgid", {NULL}, "TPGID", NULL, 0, kvar, KOFF(ki_tpgid), UINT, PIDFMT},
+ {"tsiz", {NULL}, "TSIZ", NULL, 0, kvar, KOFF(ki_tsize), PGTOK, "ld"},
+ {"tt", {NULL}, "TT ", NULL, 0, tname, 0, UNSPEC, NULL},
+ {"tty", {NULL}, "TTY", NULL, LJUST, longtname, 0, UNSPEC, NULL},
+ {"ucomm", {NULL}, "UCOMM", NULL, LJUST, ucomm, 0, UNSPEC, NULL},
+ {"uid", {NULL}, "UID", NULL, 0, kvar, KOFF(ki_uid), UINT, UIDFMT},
+ {"upr", {NULL}, "UPR", NULL, 0, upr, 0, UNSPEC, NULL},
+ {"user", {NULL}, "USER", NULL, LJUST, username, 0, UNSPEC, NULL},
+ {"usertime", {NULL}, "USERTIME", NULL, USER, usertime, 0, UNSPEC, NULL},
+ {"vsize", {"vsz"}, NULL, NULL, 0, NULL, 0, UNSPEC, NULL},
+ {"vsz", {NULL}, "VSZ", NULL, 0, vsize, 0, UNSPEC, NULL},
+ {"wchan", {NULL}, "WCHAN", NULL, LJUST, wchan, 0, UNSPEC, NULL},
+};
+
+const size_t known_keywords_nb = nitems(keywords);
+
+size_t
+aliased_keyword_index(const VAR *const v)
+{
+ const VAR *const fv = (v->flag & RESOLVED_ALIAS) == 0 ? v : v->final_kw;
+ const size_t idx = fv - keywords;
+ assert(idx < known_keywords_nb);
+ return (idx);
+}
+
+void
+check_keywords(void)
+{
+ const VAR *k, *next_k;
+ bool order_violated = false;
+ for (size_t i = 0; i < known_keywords_nb - 1; ++i) {
+ k = &keywords[i];
+ next_k = &keywords[i + 1];
+ if (strcmp(k->name, next_k->name) >= 0) {
+ warnx("keywords bad order: '%s' followed by '%s'", k->name, next_k->name);
+ order_violated = true;
+ }
+ }
+ if (order_violated) errx(2, "keywords not in order");
+}
+
+static void
+merge_alias(VAR *const k, VAR *const tgt)
+{
+ if ((tgt->flag & RESOLVED_ALIAS) != 0)
+ k->final_kw = tgt->final_kw;
+ else
+ k->final_kw = tgt;
+
+ if (k->header == NULL) k->header = tgt->header;
+ if (k->field == NULL) k->field = tgt->field;
+ if (k->flag == 0) k->flag = tgt->flag;
+
+ k->oproc = tgt->oproc;
+ k->off = tgt->off;
+ k->type = tgt->type;
+ k->fmt = tgt->fmt;
+}
+
+static void
+resolve_alias(VAR *const k)
+{
+ VAR *t, key;
+ if ((k->flag & RESOLVED_ALIAS) != 0 || k->aliased == NULL) return;
+ if ((k->flag & RESOLVING_ALIAS) != 0) errx(2, "cycle in alias '%s'", k->name);
+ k->flag |= RESOLVING_ALIAS;
+ key.name = k->aliased;
+ t = bsearch(&key, keywords, known_keywords_nb, sizeof(VAR), vcmp);
+ if (t == NULL) errx(2, "unknown alias target '%s'", k->aliased);
+ resolve_alias(t);
+ merge_alias(k, t);
+ k->flag &= ~RESOLVING_ALIAS;
+ k->flag |= RESOLVED_ALIAS;
+}
+
+void
+resolve_aliases(void)
+{
+ for (size_t i = 0; i < known_keywords_nb; ++i)
+ resolve_alias(&keywords[i]);
+}
+
+void
+showkey(void)
+{
+ const VAR *v;
+ int i = 0;
+ printf("Keywords:\n");
+ for (v = keywords; v < keywords + known_keywords_nb; ++v) {
+ printf("%-10s%s", v->name, (++i % 7 == 0) ? "\n" : " ");
+ }
+ printf("\n");
+}
+
+void
+parsefmt(const char *p, struct velisthead *const var_list, const int user)
+{
+ char *copy = strdup(p);
+ char *cp = copy;
+ char *token;
+ VAR *v, key;
+ struct varent *vent;
+
+ while ((token = strsep(&cp, " \t,\n")) != NULL) {
+ if (*token == '\0') continue;
+ char *hdr = strchr(token, '=');
+ if (hdr) *hdr++ = '\0';
+
+ key.name = token;
+ v = bsearch(&key, keywords, known_keywords_nb, sizeof(VAR), vcmp);
+ if (v == NULL) {
+ warnx("%s: keyword not found", token);
+ eval = 1;
+ continue;
+ }
+ resolve_alias(v);
+ vent = malloc(sizeof(struct varent));
+ if (!vent) err(1, "malloc");
+ vent->header = hdr ? strdup(hdr) : v->header;
+ vent->width = strlen(vent->header);
+ vent->var = v;
+ vent->flags = user ? VE_KEEP : 0;
+ STAILQ_INSERT_TAIL(var_list, vent, next_ve);
+ }
+ free(copy);
+}
+
+static int
+vcmp(const void *a, const void *b)
+{
+ return strcmp(((const VAR *)a)->name, ((const VAR *)b)->name);
+}
diff --git a/corebinutils/ps/nlist.c b/corebinutils/ps/nlist.c
new file mode 100644
index 0000000000..ae16581981
--- /dev/null
+++ b/corebinutils/ps/nlist.c
@@ -0,0 +1,76 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1990, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * 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.
+ */
+
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include "ps.h"
+
+fixpt_t ccpu = 0;
+int nlistread = 0;
+unsigned long mem_total_kb = 0;
+int fscale = 100;
+
+/*
+ * On Linux, we read memory stats from /proc/meminfo.
+ * ccpu and fscale are used for CPU calculations; we'll use a simplified model.
+ */
+int
+donlist(void)
+{
+ FILE *fp;
+ char line[256];
+
+ if (nlistread)
+ return (0);
+
+ fp = fopen("/proc/meminfo", "r");
+ if (fp) {
+ while (fgets(line, sizeof(line), fp)) {
+ if (sscanf(line, "MemTotal: %lu", &mem_total_kb) == 1) {
+ break;
+ }
+ }
+ fclose(fp);
+ }
+
+ if (mem_total_kb == 0)
+ mem_total_kb = 1024 * 256; /* fallback */
+
+ /*
+ * Linux fixed-point scale for load averages is usually 1 << 8,
+ * but for ps pct calculation we just need a baseline.
+ */
+ fscale = 100;
+ nlistread = 1;
+ return (0);
+}
diff --git a/corebinutils/ps/print.c b/corebinutils/ps/print.c
new file mode 100644
index 0000000000..b9fd40d300
--- /dev/null
+++ b/corebinutils/ps/print.c
@@ -0,0 +1,596 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1990, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * 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.
+ */
+
+#include <sys/param.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/sysmacros.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
+#include <err.h>
+#include <grp.h>
+#include <langinfo.h>
+#include <locale.h>
+#include <math.h>
+#include <pwd.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+
+#include "ps.h"
+
+#define COMMAND_WIDTH 16
+#define ARGUMENTS_WIDTH 16
+
+#define SAFE_ASPRINTF(ptr, fmt, ...) do { \
+ if (asprintf(ptr, fmt, __VA_ARGS__) == -1) \
+ err(1, "asprintf"); \
+} while (0)
+
+/* Simple strvis-like implementation */
+static char *
+simple_strvis(const char *src)
+{
+ if (!src) return NULL;
+ size_t len = strlen(src);
+ char *dst = malloc(len * 4 + 1);
+ if (!dst) return NULL;
+ char *d = dst;
+ for (const char *s = src; *s; s++) {
+ if (isprint((unsigned char)*s) || *s == ' ') {
+ *d++ = *s;
+ } else {
+ d += sprintf(d, "\\%03o", (unsigned char)*s);
+ }
+ }
+ *d = '\0';
+ return dst;
+}
+
+void
+printheader(void)
+{
+ struct varent *vent;
+
+ STAILQ_FOREACH(vent, &varlist, next_ve)
+ if (*vent->header != '\0')
+ break;
+ if (!vent)
+ return;
+
+ STAILQ_FOREACH(vent, &varlist, next_ve) {
+ const VAR *v = vent->var;
+ if (v->flag & LJUST) {
+ if (STAILQ_NEXT(vent, next_ve) == NULL) /* last one */
+ printf("%s", vent->header);
+ else
+ printf("%-*s", vent->width, vent->header);
+ } else
+ printf("%*s", vent->width, vent->header);
+ if (STAILQ_NEXT(vent, next_ve) != NULL)
+ printf(" ");
+ }
+ printf("\n");
+}
+
+char *
+arguments(KINFO *k, VARENT *ve)
+{
+ char *vis_args = simple_strvis(k->ki_args);
+ if (!vis_args) return NULL;
+
+ if (STAILQ_NEXT(ve, next_ve) != NULL && strlen(vis_args) > ARGUMENTS_WIDTH)
+ vis_args[ARGUMENTS_WIDTH] = '\0';
+
+ return (vis_args);
+}
+
+char *
+command(KINFO *k, VARENT *ve)
+{
+ char *str = NULL;
+
+ if (cflag) {
+ if (STAILQ_NEXT(ve, next_ve) == NULL) {
+ SAFE_ASPRINTF(&str, "%s%s",
+ k->ki_d.prefix ? k->ki_d.prefix : "",
+ k->ki_p->ki_comm);
+ } else {
+ str = strdup(k->ki_p->ki_comm);
+ if (!str) err(1, "strdup");
+ }
+ return (str);
+ }
+
+ char *vis_args = simple_strvis(k->ki_args);
+ if (!vis_args) return strdup("-");
+
+ if (STAILQ_NEXT(ve, next_ve) == NULL) {
+ /* last field */
+ char *vis_env = k->ki_env ? simple_strvis(k->ki_env) : NULL;
+ asprintf(&str, "%s%s%s%s",
+ k->ki_d.prefix ? k->ki_d.prefix : "",
+ vis_env ? vis_env : "",
+ vis_env ? " " : "",
+ vis_args);
+ free(vis_env);
+ free(vis_args);
+ } else {
+ str = vis_args;
+ if (strlen(str) > COMMAND_WIDTH)
+ str[COMMAND_WIDTH] = '\0';
+ }
+ return (str);
+}
+
+char *
+ucomm(KINFO *k, VARENT *ve)
+{
+ char *str;
+ if (STAILQ_NEXT(ve, next_ve) == NULL) {
+ asprintf(&str, "%s%s",
+ k->ki_d.prefix ? k->ki_d.prefix : "",
+ k->ki_p->ki_comm);
+ } else {
+ str = strdup(k->ki_p->ki_comm);
+ }
+ return (str);
+}
+
+char *
+tdnam(KINFO *k, VARENT *ve __unused)
+{
+ if (k->ki_p->ki_numthreads > 1)
+ return strdup(k->ki_p->ki_tdname);
+ return strdup(" ");
+}
+
+char *
+logname(KINFO *k, VARENT *ve __unused)
+{
+ if (*k->ki_p->ki_login == '\0')
+ return NULL;
+ return strdup(k->ki_p->ki_login);
+}
+
+char *
+state(KINFO *k, VARENT *ve __unused)
+{
+ char *buf = malloc(16);
+ if (!buf) return NULL;
+ char *cp = buf;
+
+ *cp++ = k->ki_p->ki_stat;
+
+ if (k->ki_p->ki_nice < 0) *cp++ = '<';
+ else if (k->ki_p->ki_nice > 0) *cp++ = 'N';
+
+ /* Approximate flags */
+ if (k->ki_p->ki_sid == k->ki_p->ki_pid) *cp++ = 's';
+ if (k->ki_p->ki_tdev != (dev_t)-1 && k->ki_p->ki_pgid == k->ki_p->ki_tpgid) *cp++ = '+';
+
+ *cp = '\0';
+ return buf;
+}
+
+char *
+pri(KINFO *k, VARENT *ve __unused)
+{
+ char *str;
+ SAFE_ASPRINTF(&str, "%d", k->ki_p->ki_pri);
+ return str;
+}
+
+char *
+upr(KINFO *k, VARENT *ve __unused)
+{
+ char *str;
+ SAFE_ASPRINTF(&str, "%d", k->ki_p->ki_pri);
+ return str;
+}
+
+char *
+username(KINFO *k, VARENT *ve __unused)
+{
+ return strdup(user_from_uid(k->ki_p->ki_uid, 0));
+}
+
+char *
+egroupname(KINFO *k, VARENT *ve __unused)
+{
+ return strdup(group_from_gid(k->ki_p->ki_groups[0], 0));
+}
+
+char *
+rgroupname(KINFO *k, VARENT *ve __unused)
+{
+ return strdup(group_from_gid(k->ki_p->ki_rgid, 0));
+}
+
+char *
+runame(KINFO *k, VARENT *ve __unused)
+{
+ return strdup(user_from_uid(k->ki_p->ki_ruid, 0));
+}
+
+char *
+tdev(KINFO *k, VARENT *ve __unused)
+{
+ char *str;
+ if (k->ki_p->ki_tdev == (dev_t)-1)
+ str = strdup("-");
+ else
+ asprintf(&str, "%#jx", (uintmax_t)k->ki_p->ki_tdev);
+ return str;
+}
+
+char *
+tname(KINFO *k, VARENT *ve __unused)
+{
+ char *name = devname(k->ki_p->ki_tdev, S_IFCHR);
+ if (!name) return strdup("- ");
+
+ char *str;
+ if (strncmp(name, "tty", 3) == 0) name += 3;
+ if (strncmp(name, "pts/", 4) == 0) name += 4;
+
+ asprintf(&str, "%s ", name);
+ return str;
+}
+
+char *
+longtname(KINFO *k, VARENT *ve __unused)
+{
+ char *name = devname(k->ki_p->ki_tdev, S_IFCHR);
+ return strdup(name ? name : "-");
+}
+
+char *
+started(KINFO *k, VARENT *ve __unused)
+{
+ if (!k->ki_valid) return NULL;
+ char *buf = malloc(100);
+ if (!buf) return NULL;
+ time_t then = k->ki_p->ki_start.tv_sec;
+ struct tm *tp = localtime(&then);
+ if (now - then < 24 * 3600)
+ strftime(buf, 100, "%H:%M ", tp);
+ else if (now - then < 7 * 86400)
+ strftime(buf, 100, "%a%H ", tp);
+ else
+ strftime(buf, 100, "%e%b%y", tp);
+ return buf;
+}
+
+char *
+lstarted(KINFO *k, VARENT *ve __unused)
+{
+ if (!k->ki_valid) return NULL;
+ char *buf = malloc(100);
+ if (!buf) return NULL;
+ time_t then = k->ki_p->ki_start.tv_sec;
+ strftime(buf, 100, "%c", localtime(&then));
+ return buf;
+}
+
+char *
+lockname(KINFO *k, VARENT *ve __unused)
+{
+ return NULL;
+}
+
+char *
+wchan(KINFO *k, VARENT *ve __unused)
+{
+ if (k->ki_p->ki_wmesg[0])
+ return strdup(k->ki_p->ki_wmesg);
+ return NULL;
+}
+
+char *
+nwchan(KINFO *k, VARENT *ve __unused)
+{
+ return NULL;
+}
+
+char *
+mwchan(KINFO *k, VARENT *ve __unused)
+{
+ if (k->ki_p->ki_wmesg[0])
+ return strdup(k->ki_p->ki_wmesg);
+ return NULL;
+}
+
+char *
+vsize(KINFO *k, VARENT *ve __unused)
+{
+ char *str;
+ asprintf(&str, "%lu", (unsigned long)(k->ki_p->ki_size / 1024));
+ return str;
+}
+
+static char *
+printtime(KINFO *k, VARENT *ve __unused, long secs, long psecs)
+{
+ char *str;
+ if (!k->ki_valid) { secs = 0; psecs = 0; }
+ asprintf(&str, "%ld:%02ld.%02ld", secs / 60, secs % 60, psecs / 10000);
+ return str;
+}
+
+char *
+cputime(KINFO *k, VARENT *ve)
+{
+ long secs = k->ki_p->ki_runtime / 1000000;
+ long psecs = k->ki_p->ki_runtime % 1000000;
+ return printtime(k, ve, secs, psecs);
+}
+
+char *
+cpunum(KINFO *k, VARENT *ve __unused)
+{
+ char *str;
+ asprintf(&str, "%d", k->ki_p->ki_lastcpu);
+ return str;
+}
+
+char *
+systime(KINFO *k, VARENT *ve)
+{
+ return strdup("0:00.00");
+}
+
+char *
+usertime(KINFO *k, VARENT *ve)
+{
+ return strdup("0:00.00");
+}
+
+char *
+elapsed(KINFO *k, VARENT *ve __unused)
+{
+ if (!k->ki_valid) return NULL;
+ time_t val = now - k->ki_p->ki_start.tv_sec;
+ int days = val / 86400; val %= 86400;
+ int hours = val / 3600; val %= 3600;
+ int mins = val / 60;
+ int secs = val % 60;
+ char *str;
+ if (days != 0) asprintf(&str, "%3d-%02d:%02d:%02d", days, hours, mins, secs);
+ else if (hours != 0) asprintf(&str, "%02d:%02d:%02d", hours, mins, secs);
+ else asprintf(&str, "%02d:%02d", mins, secs);
+ return str;
+}
+
+char *
+elapseds(KINFO *k, VARENT *ve __unused)
+{
+ if (!k->ki_valid) return NULL;
+ char *str;
+ asprintf(&str, "%jd", (intmax_t)(now - k->ki_p->ki_start.tv_sec));
+ return str;
+}
+
+double
+getpcpu(const KINFO *k)
+{
+ if (!k->ki_valid) return 0.0;
+ time_t uptime = now - k->ki_p->ki_start.tv_sec;
+ if (uptime <= 0) uptime = 1;
+ return (100.0 * (k->ki_p->ki_runtime / 1000000.0)) / uptime;
+}
+
+char *
+pcpu(KINFO *k, VARENT *ve __unused)
+{
+ char *str;
+ SAFE_ASPRINTF(&str, "%.1f", k->ki_pcpu);
+ return str;
+}
+
+char *
+pmem(KINFO *k, VARENT *ve __unused)
+{
+ if (mem_total_kb == 0) donlist();
+ char *str;
+ double pct = (100.0 * k->ki_p->ki_rssize) / mem_total_kb;
+ asprintf(&str, "%.1f", pct);
+ return str;
+}
+
+char *
+pagein(KINFO *k, VARENT *ve __unused)
+{
+ return strdup("0");
+}
+
+char *
+maxrss(KINFO *k, VARENT *ve __unused)
+{
+ return NULL;
+}
+
+char *
+priorityr(KINFO *k, VARENT *ve __unused)
+{
+ char *str;
+ SAFE_ASPRINTF(&str, "%d", k->ki_p->ki_pri);
+ return str;
+}
+
+char *
+kvar(KINFO *k, VARENT *ve)
+{
+ const VAR *v = ve->var;
+ char *bp = (char *)k->ki_p + v->off;
+ char *str;
+ char fmt[32];
+ snprintf(fmt, sizeof(fmt), "%%%s", v->fmt ? v->fmt : "s");
+ switch (v->type) {
+ case INT: asprintf(&str, fmt, *(int *)bp); break;
+ case UINT: asprintf(&str, fmt, *(unsigned int *)bp); break;
+ case LONG: asprintf(&str, fmt, *(long *)bp); break;
+ case ULONG: asprintf(&str, fmt, *(unsigned long *)bp); break;
+ case SHORT: asprintf(&str, fmt, *(short *)bp); break;
+ case USHORT: asprintf(&str, fmt, *(unsigned short *)bp); break;
+ case CHAR: asprintf(&str, fmt, *(char *)bp); break;
+ case UCHAR: asprintf(&str, fmt, *(unsigned char *)bp); break;
+ case PGTOK:
+ {
+ unsigned long val = *(unsigned long *)bp;
+ asprintf(&str, fmt, (unsigned long)(val * getpagesize() / 1024));
+ break;
+ }
+ default: asprintf(&str, "?"); break;
+ }
+ return str;
+}
+
+char *
+rvar(KINFO *k, VARENT *ve)
+{
+ return strdup("-");
+}
+
+char *
+emulname(KINFO *k, VARENT *ve)
+{
+ return strdup("Linux");
+}
+
+char *
+jailname(KINFO *k, VARENT *ve)
+{
+ return strdup("-");
+}
+
+char *
+label(KINFO *k, VARENT *ve)
+{
+ return strdup("-");
+}
+
+char *
+loginclass(KINFO *k, VARENT *ve)
+{
+ return strdup("-");
+}
+
+/* Linux-specific helpers */
+
+char *
+user_from_uid(uid_t uid, int nouser)
+{
+ static char buf[32];
+ struct passwd *pw = getpwuid(uid);
+ if (!pw || nouser) {
+ snprintf(buf, sizeof(buf), "%u", uid);
+ return buf;
+ }
+ return pw->pw_name;
+}
+
+char *
+group_from_gid(gid_t gid, int nogroup)
+{
+ static char buf[32];
+ struct group *gr = getgrgid(gid);
+ if (!gr || nogroup) {
+ snprintf(buf, sizeof(buf), "%u", gid);
+ return buf;
+ }
+ return gr->gr_name;
+}
+
+struct dev_cache {
+ dev_t dev;
+ char name[PATH_MAX];
+};
+
+static struct dev_cache *dcache = NULL;
+static int dcache_size = 0;
+
+static void
+cache_dir(const char *path, const char *prefix)
+{
+ DIR *dir = opendir(path);
+ struct dirent *ent;
+ struct stat st;
+ char buf[PATH_MAX];
+
+ if (!dir) return;
+ while ((ent = readdir(dir))) {
+ if (ent->d_name[0] == '.') continue;
+ snprintf(buf, sizeof(buf), "%s/%s", path, ent->d_name);
+ if (stat(buf, &st) == 0 && S_ISCHR(st.st_mode)) {
+ struct dev_cache *tmp;
+ tmp = realloc(dcache, (dcache_size + 1) * sizeof(struct dev_cache));
+ if (!tmp) err(1, "realloc");
+ dcache = tmp;
+ dcache[dcache_size].dev = st.st_rdev;
+ snprintf(dcache[dcache_size].name, PATH_MAX, "%s%s", prefix, ent->d_name);
+ dcache_size++;
+ }
+ }
+ closedir(dir);
+}
+
+char *
+devname(dev_t dev, mode_t type)
+{
+ if (dev == (dev_t)-1) return NULL;
+ if (dcache == NULL) {
+ cache_dir("/dev/pts", "pts/");
+ cache_dir("/dev", "");
+ }
+ for (int i = 0; i < dcache_size; i++) {
+ if (dcache[i].dev == dev)
+ return dcache[i].name;
+ }
+ return NULL;
+}
+
+void
+free_devnames(void)
+{
+ free(dcache);
+ dcache = NULL;
+ dcache_size = 0;
+}
diff --git a/corebinutils/ps/ps.1 b/corebinutils/ps/ps.1
new file mode 100644
index 0000000000..fb93a6b797
--- /dev/null
+++ b/corebinutils/ps/ps.1
@@ -0,0 +1,1020 @@
+.\"-
+.\" SPDX-License-Identifier: BSD-3-Clause
+.\"
+.\" Copyright (c) 1980, 1990, 1991, 1993, 1994
+.\" The Regents of the University of California. All rights reserved.
+.\" Copyright (c) 2025 The FreeBSD Foundation
+.\" Copyright (c) 2026 Project Tick.
+.\"
+.\" Portions of this documentation were written by Olivier Certner
+.\" <olce@FreeBSD.org> at Kumacom SARL under sponsorship from the FreeBSD
+ \" Foundation.
+.\"
+.\" 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 July 16, 2025
+.Dt PS 1
+.Os
+.Sh NAME
+.Nm ps
+.Nd process status
+.Sh SYNOPSIS
+.Nm
+.Op Fl -libxo
+.Op Fl AaCcdefHhjlmrSTuvwXxZ
+.Op Fl O Ar fmt
+.Op Fl o Ar fmt
+.Op Fl D Ar up | down | both
+.Op Fl G Ar gid Ns Op , Ns Ar gid Ns Ar ...
+.Op Fl J Ar jid Ns Op , Ns Ar jid Ns Ar ...
+.Op Fl M Ar core
+.Op Fl N Ar system
+.Op Fl p Ar pid Ns Op , Ns Ar pid Ns Ar ...
+.Op Fl t Ar tty Ns Op , Ns Ar tty Ns Ar ...
+.Op Fl U Ar user Ns Op , Ns Ar user Ns Ar ...
+.Nm
+.Op Fl -libxo
+.Fl L
+.Sh DESCRIPTION
+The
+.Nm
+utility displays information about a selection of processes.
+Its traditional text style output consists of a header line followed by one line
+of information per selected process, or possibly multiple ones if using
+.Fl H
+.Pq one per lightweight-process .
+Other output styles can be requested via
+.Fl -libxo .
+.Pp
+By default, only the processes of the calling user, determined by matching their
+effective user ID with that of the
+.Nm
+process, that have controlling terminals are shown.
+A different set of processes can be selected for display by using combinations
+of the
+.Fl A , a , D , G , J , p , T , t , U , X ,
+and
+.Fl x
+options.
+Except for options
+.Fl X
+and
+.Fl x ,
+as soon as one of them appears, it inhibits the default process selection, i.e.,
+the calling user's processes are shown only on request.
+If more than one of these
+.Pq with same exceptions
+appear,
+.Nm
+will select processes as soon as they are matched by at least one of them
+.Pq inclusive OR .
+The
+.Fl X
+option can be independently used to further filter the listed processes to only
+those that have a controlling terminal
+.Po
+except for those selected by
+.Fl p
+.Pc .
+Its opposite,
+.Fl x ,
+forcefully removes that filter.
+If none of
+.Fl X
+and
+.Fl x
+is specified, the implied default behavior is that of
+.Fl X
+unless using another option whose description explicitly says that
+.Fl x
+is implied.
+.Pp
+For each selected process, the default displayed information consists of the
+process' ID, controlling terminal, state, CPU time
+.Pq including both user and system time
+and associated command
+.Po
+see the documentation for the
+.Cm command
+keyword below
+.Pc .
+This information can be tweaked using two groups of options which can be
+combined as needed.
+First, options
+.Fl o
+and
+.Fl O
+add columns with data corresponding to the explicitly passed keywords.
+Available keywords are documented in the
+.Sx KEYWORDS
+section below.
+They can be listed using option
+.Fl L .
+Second, options
+.Fl j , l , u ,
+and
+.Fl v
+designate specific predefined groups of columns, also called canned displays.
+Appearance of any of these options inhibits the default display, replacing it
+all with the requested columns, and in the order options are passed.
+The individual columns requested via a canned display option that have the same
+keyword or an alias to that of some column added by an earlier canned display
+option, or by an explicit
+.Fl O
+or
+.Fl o
+option anywhere on the command line, are suppressed.
+This automatic removal of duplicate data in canned displays is useful for
+slightly tweaking these displays and/or combining multiple ones without having
+to rebuild variants from scratch, e.g., using only
+.Fl o
+options.
+.Pp
+Output information lines are by default sorted first by controlling terminal,
+then by process ID, and then, if
+.Fl H
+has been specified, by lightweight-process (thread) ID.
+The
+.Fl m , r , u ,
+and
+.Fl v
+options will change the sort order.
+If more than one sorting option was given, then the selected processes
+will be sorted by the last sorting option which was specified.
+.Pp
+If the traditional text output (the default) is used, the default output width is that requested by the
+.Ev COLUMNS
+environment variable if present, else the line width of the terminal associated
+to the
+.Nm
+process, if any.
+In all other situations, the output width is unlimited.
+See also the
+.Fl w
+option and the
+.Sx BUGS
+section.
+.Pp
+For backwards compatibility,
+.Nm
+attempts to interpret any positional argument as a process ID, as if specified
+by the
+.Fl p
+option.
+Failure to do so will trigger an error.
+.Nm
+also accepts the old-style BSD options, whose format and effect are left
+undocumented on purpose.
+.Pp
+The options are as follows:
+.Bl -tag -width indent
+.It Fl -libxo
+Generate output via
+.Xr libxo 3
+in a selection of different human and machine readable formats.
+See
+.Xr xo_options 7
+for details on command line arguments.
+The default is the traditional text style output.
+.It Fl A
+Display information about all processes in the system.
+Using this option is strictly equivalent to specifying both
+.Fl a
+and
+.Fl x .
+Please see their description for more information.
+.It Fl a
+Display information about all users' processes.
+It does not, however, list all processes
+.Po
+see
+.Fl A
+and
+.Fl x
+.Pc .
+If the
+.Va security.bsd.see_other_uids
+sysctl is set to zero, this option is honored only if the real user ID of the
+.Nm
+process is 0.
+.It Fl C
+Change the way the CPU percentage is calculated by using a
+.Dq raw
+CPU calculation that ignores
+.Dq resident
+time (this normally has
+no effect).
+.It Fl c
+Change the
+.Dq command
+column output to just contain the executable name,
+rather than the full command line.
+.It Fl D
+Expand the list of selected processes based on the process tree.
+.Dq UP
+will add the ancestor processes,
+.Dq DOWN
+will add the descendant processes, and
+.Dq BOTH
+will add both the ancestor and the descendant processes.
+.Fl D
+does not imply
+.Fl d ,
+but works well with it.
+.It Fl d
+Arrange processes into descendancy order and prefix each command with
+indentation text showing sibling and parent/child relationships as a tree.
+If either of the
+.Fl m
+and
+.Fl r
+options are also used, they control how sibling processes are sorted
+relative to each other.
+Note that this option has no effect if the last column does not have
+.Cm comm ,
+.Cm command
+or
+.Cm ucomm
+as its keyword.
+.It Fl e
+Display the environment as well.
+.It Fl f
+Indicates to print the full command and arguments in
+.Cm command
+columns.
+This is the default behavior on
+.Fx .
+See
+.Fl c
+to turn it off.
+.It Fl G
+Display information about processes whose real group ID matches the specified
+group IDs or names.
+Implies
+.Fl x
+by default.
+.It Fl H
+Show all of the threads associated with each process.
+.It Fl h
+Repeat the information header as often as necessary to guarantee one
+header per page of information.
+.It Fl J
+Display information about processes which match the specified jail IDs.
+This may be either the
+.Cm jid
+or
+.Cm name
+of the jail.
+Use
+.Fl J
+.Sy 0
+to request display of host processes.
+Implies
+.Fl x
+by default.
+.It Fl j
+Print information associated with the following keywords:
+.Cm user , pid , ppid , pgid , sid , jobc , state , tt , time ,
+and
+.Cm command .
+.It Fl L
+List the set of keywords available for the
+.Fl O
+and
+.Fl o
+options.
+.It Fl l
+Display information associated with the following keywords:
+.Cm uid , pid , ppid , cpu , pri , nice , vsz , rss , mwchan , state ,
+.Cm tt , time ,
+and
+.Cm command .
+.It Fl M
+Extract values associated with the name list from the specified core
+instead of the currently running system.
+.It Fl m
+Sort by memory usage, instead of the combination of controlling
+terminal and process ID.
+.It Fl N
+Extract the name list from the specified system instead of the default,
+which is the kernel image the system has booted from.
+.It Fl O
+Save passed columns in a separate list that in the end is grafted just after the
+display's first occurence of the process ID column as specified by other
+options, or the default display if there is none.
+If the display prepared by other options does not include a process ID column,
+the list is inserted at start of the display.
+Further occurences of
+.Fl O
+append to the to-be-grafted list of columns.
+This option takes a space- or comma-separated list of keywords.
+The last keyword in the list may be appended with an equals sign
+.Pq Ql =
+as explained for option
+.Fl o
+and with the same effect.
+.It Fl o
+Display information associated with the space- or comma-separated list of
+keywords specified.
+The last keyword in the list may be appended with an equals sign
+.Pq Ql =
+and a string that spans the rest of the argument, and can contain
+space and comma characters.
+This causes the printed header to use the specified string instead of
+the standard header.
+Multiple keywords may also be given in the form of more than one
+.Fl o
+option.
+So the header texts for multiple keywords can be changed.
+If all keywords have empty header texts, no header line is written.
+.It Fl p
+Display information about processes which match the specified process IDs.
+Processes selected by this option are not subject to being filtered by
+.Fl X .
+.It Fl r
+Sort by current CPU usage, instead of the combination of controlling
+terminal and process ID.
+.It Fl S
+Change the way the process times, namely cputime, systime, and usertime,
+are calculated by summing all exited children to their parent process.
+.It Fl T
+Display information about processes attached to the device associated
+with the standard input.
+.It Fl t
+Display information about processes attached to the specified terminal
+devices.
+Full pathnames, as well as abbreviations (see explanation of the
+.Cm tt
+keyword) can be specified.
+Implies
+.Fl x
+by default.
+.It Fl U
+Display information about processes whose real user ID matches the specified
+user IDs or names.
+Implies
+.Fl x
+by default.
+.It Fl u
+Display information associated with the following keywords:
+.Cm user , pid , %cpu , %mem , vsz , rss , tt , state , start , time ,
+and
+.Cm command .
+The
+.Fl u
+option implies the
+.Fl r
+option.
+.It Fl v
+Display information associated with the following keywords:
+.Cm pid , state , time , sl , re , pagein , vsz , rss , lim , tsiz ,
+.Cm %cpu , %mem ,
+and
+.Cm command .
+The
+.Fl v
+option implies the
+.Fl m
+option.
+.It Fl w
+Use at least 131 columns to display information.
+If
+.Fl w
+is specified more than once,
+.Nm
+will use as many columns as necessary.
+Please see the preamble of this manual page for how the output width is
+initially determined.
+In particular, if the initial output width is unlimited, specifying
+.Fl w
+has no effect.
+Please also consult the
+.Sx BUGS
+section.
+.It Fl X
+When displaying processes selected by other options, skip any processes which do
+not have a controlling terminal, except for those selected through
+.Fl p .
+This is the default behaviour, unless using another option whose description
+explicitly says that
+.Fl x
+is implied.
+.It Fl x
+When displaying processes selected by other options, include processes which do
+not have a controlling terminal.
+This option has the opposite behavior to that of
+.Fl X .
+If both
+.Fl X
+and
+.Fl x
+are specified,
+.Nm
+will obey the last occurence.
+.It Fl Z
+Add
+.Xr mac 4
+label to the list of keywords for which
+.Nm
+will display information.
+.El
+.Sh KEYWORDS
+The following is a complete list of the available keywords and their meanings.
+Several of them have aliases (keywords which are synonyms).
+Detailed descriptions for some of them can be found after this list.
+.Pp
+.Bl -tag -width ".Cm sigignore" -compact
+.It Cm %cpu
+percentage CPU usage (alias
+.Cm pcpu )
+.It Cm %mem
+percentage memory usage (alias
+.Cm pmem )
+.It Cm acflag
+accounting flag (alias
+.Cm acflg )
+.It Cm args
+command and arguments
+.It Cm class
+login class
+.It Cm comm
+command
+.It Cm command
+command and arguments
+.It Cm cow
+number of copy-on-write faults
+.It Cm cpu
+The processor number on which the process is executing (visible only on SMP
+systems).
+.It Cm dsiz
+data size in KiB
+.It Cm emul
+system-call emulation environment (ABI)
+.It Cm etime
+elapsed running time, format
+.Do
+.Op days- Ns
+.Op hours\&: Ns
+minutes:seconds
+.Dc
+.It Cm etimes
+elapsed running time, in decimal integer seconds
+.It Cm fib
+default FIB number, see
+.Xr setfib 1
+.It Cm flags
+the process flags, in hexadecimal (alias
+.Cm f )
+.It Cm flags2
+the additional set of process flags, in hexadecimal (alias
+.Cm f2 )
+.It Cm gid
+effective group ID (alias
+.Cm egid )
+.It Cm group
+group name (from egid) (alias
+.Cm egroup )
+.It Cm inblk
+total blocks read (alias
+.Cm inblock )
+.It Cm jail
+jail name
+.It Cm jid
+jail ID
+.It Cm jobc
+job control count
+.It Cm ktrace
+tracing flags
+.It Cm label
+MAC label
+.It Cm lim
+memoryuse limit
+.It Cm lockname
+lock currently blocked on (as a symbolic name)
+.It Cm logname
+login name of user who started the session
+.It Cm lstart
+time started
+.It Cm lwp
+thread (light-weight process) ID (alias
+.Cm tid )
+.It Cm majflt
+total page faults
+.It Cm minflt
+total page reclaims
+.It Cm msgrcv
+total messages received (reads from pipes/sockets)
+.It Cm msgsnd
+total messages sent (writes on pipes/sockets)
+.It Cm mwchan
+wait channel or lock currently blocked on
+.It Cm nice
+nice value (alias
+.Cm ni )
+.It Cm nivcsw
+total involuntary context switches
+.It Cm nlwp
+number of threads (light-weight processes) tied to a process
+.It Cm nsigs
+total signals taken (alias
+.Cm nsignals )
+.It Cm nswap
+total swaps in/out
+.It Cm nvcsw
+total voluntary context switches
+.It Cm nwchan
+wait channel (as an address)
+.It Cm oublk
+total blocks written (alias
+.Cm oublock )
+.It Cm paddr
+process pointer
+.It Cm pagein
+pageins (same as majflt)
+.It Cm pgid
+process group number
+.It Cm pid
+process ID
+.It Cm ppid
+parent process ID
+.It Cm pri
+scheduling priority
+.It Cm re
+core residency time (in seconds; 127 = infinity)
+.It Cm rgid
+real group ID
+.It Cm rgroup
+group name (from rgid)
+.It Cm rss
+resident set size in KiB
+.It Cm rtprio
+realtime priority (see
+.Xr rtprio 1)
+.It Cm ruid
+real user ID
+.It Cm ruser
+user name (from ruid)
+.It Cm sid
+session ID
+.It Cm sig
+pending signals (alias
+.Cm pending )
+.It Cm sigcatch
+caught signals (alias
+.Cm caught )
+.It Cm sigignore
+ignored signals (alias
+.Cm ignored )
+.It Cm sigmask
+blocked signals (alias
+.Cm blocked )
+.It Cm sl
+sleep time (in seconds; 127 = infinity)
+.It Cm ssiz
+stack size in KiB
+.It Cm start
+time started
+.It Cm state
+symbolic process state (alias
+.Cm stat )
+.It Cm svgid
+saved gid from a setgid executable
+.It Cm svuid
+saved UID from a setuid executable
+.It Cm systime
+accumulated system CPU time
+.It Cm tdaddr
+thread address
+.It Cm tdname
+thread name
+.It Cm tdev
+control terminal device number
+.It Cm time
+accumulated CPU time, user + system (alias
+.Cm cputime )
+.It Cm tpgid
+control terminal process group ID
+.It Cm tracer
+tracer process ID
+.\".It Cm trss
+.\"text resident set size in KiB
+.It Cm tsid
+control terminal session ID
+.It Cm tsiz
+text size in KiB
+.It Cm tt
+control terminal name (two letter abbreviation)
+.It Cm tty
+full name of control terminal
+.It Cm ucomm
+process name used for accounting
+.It Cm uid
+effective user ID (alias
+.Cm euid )
+.It Cm upr
+scheduling priority on return from system call (alias
+.Cm usrpri )
+.It Cm uprocp
+process pointer
+.It Cm user
+user name (from UID)
+.It Cm usertime
+accumulated user CPU time
+.It Cm vmaddr
+vmspace pointer
+.It Cm vsz
+virtual size in KiB (alias
+.Cm vsize )
+.It Cm wchan
+wait channel (as a symbolic name)
+.It Cm xstat
+exit or stop status (valid only for stopped or zombie process)
+.El
+.Pp
+Some of these keywords are further specified as follows:
+.Bl -tag -width lockname
+.It Cm %cpu
+The CPU utilization of the process; this is a decaying average over up to
+a minute of previous (real) time.
+Since the time base over which this is computed varies (since processes may
+be very young) it is possible for the sum of all
+.Cm %cpu
+fields to exceed 100%.
+.It Cm %mem
+The percentage of real memory used by this process.
+.It Cm class
+Login class associated with the process.
+.It Cm command
+The printed command and arguments are determined as follows.
+A process that has exited and has a parent that has not yet waited for the
+process (in other words, a zombie) is listed as
+.Dq Li <defunct>.
+If the arguments cannot be located
+.Po
+usually because they have not been set, as is the case for system processes
+and/or kernel threads
+.Pc ,
+the command name is printed within square brackets.
+The
+.Nm
+utility first tries to obtain the arguments cached by the kernel
+.Po
+if they were shorter than the value of the
+.Va kern.ps_arg_cache_limit
+sysctl
+.Pc .
+The process can change the arguments shown with
+.Xr setproctitle 3 .
+Otherwise,
+.Nm
+makes an educated guess as to the file name and arguments given when the
+process was created by examining memory or the swap area.
+The method is inherently somewhat unreliable and in any event a process
+is entitled to destroy this information.
+The
+.Cm ucomm
+keyword
+.Pq accounting
+can, however, be depended on.
+If the arguments are unavailable or do not agree with the
+.Cm ucomm
+keyword, the value for the
+.Cm ucomm
+keyword is appended to the arguments in parentheses.
+.It Cm flags
+The flags associated with the process as in
+the include file
+.In sys/proc.h :
+.Bl -column P_SINGLE_BOUNDARY 0x40000000
+.It Dv "P_ADVLOCK" Ta No "0x00000001" Ta "Process may hold a POSIX advisory lock"
+.It Dv "P_CONTROLT" Ta No "0x00000002" Ta "Has a controlling terminal"
+.It Dv "P_KPROC" Ta No "0x00000004" Ta "Kernel process"
+.It Dv "P_PPWAIT" Ta No "0x00000010" Ta "Parent is waiting for child to exec/exit"
+.It Dv "P_PROFIL" Ta No "0x00000020" Ta "Has started profiling"
+.It Dv "P_STOPPROF" Ta No "0x00000040" Ta "Has thread in requesting to stop prof"
+.It Dv "P_HADTHREADS" Ta No "0x00000080" Ta "Has had threads (no cleanup shortcuts)"
+.It Dv "P_SUGID" Ta No "0x00000100" Ta "Had set id privileges since last exec"
+.It Dv "P_SYSTEM" Ta No "0x00000200" Ta "System proc: no sigs, stats or swapping"
+.It Dv "P_SINGLE_EXIT" Ta No "0x00000400" Ta "Threads suspending should exit, not wait"
+.It Dv "P_TRACED" Ta No "0x00000800" Ta "Debugged process being traced"
+.It Dv "P_WAITED" Ta No "0x00001000" Ta "Someone is waiting for us"
+.It Dv "P_WEXIT" Ta No "0x00002000" Ta "Working on exiting"
+.It Dv "P_EXEC" Ta No "0x00004000" Ta "Process called exec"
+.It Dv "P_WKILLED" Ta No "0x00008000" Ta "Killed, shall go to kernel/user boundary ASAP"
+.It Dv "P_CONTINUED" Ta No "0x00010000" Ta "Proc has continued from a stopped state"
+.It Dv "P_STOPPED_SIG" Ta No "0x00020000" Ta "Stopped due to SIGSTOP/SIGTSTP"
+.It Dv "P_STOPPED_TRACE" Ta No "0x00040000" Ta "Stopped because of tracing"
+.It Dv "P_STOPPED_SINGLE" Ta No "0x00080000" Ta "Only one thread can continue"
+.It Dv "P_PROTECTED" Ta No "0x00100000" Ta "Do not kill on memory overcommit"
+.It Dv "P_SIGEVENT" Ta No "0x00200000" Ta "Process pending signals changed"
+.It Dv "P_SINGLE_BOUNDARY" Ta No "0x00400000" Ta "Threads should suspend at user boundary"
+.It Dv "P_HWPMC" Ta No "0x00800000" Ta "Process is using HWPMCs"
+.It Dv "P_JAILED" Ta No "0x01000000" Ta "Process is in jail"
+.It Dv "P_TOTAL_STOP" Ta No "0x02000000" Ta "Stopped for system suspend"
+.It Dv "P_INEXEC" Ta No "0x04000000" Ta Process is in Xr execve 2
+.It Dv "P_STATCHILD" Ta No "0x08000000" Ta "Child process stopped or exited"
+.It Dv "P_INMEM" Ta No "0x10000000" Ta "Always set, unused"
+.It Dv "P_PPTRACE" Ta No "0x80000000" Ta "Vforked child issued ptrace(PT_TRACEME)"
+.El
+.It Cm flags2
+The flags kept in
+.Va p_flag2
+associated with the process as in
+the include file
+.In sys/proc.h :
+.Bl -column P2_INHERIT_PROTECTED 0x00000001
+.It Dv "P2_INHERIT_PROTECTED" Ta No "0x00000001" Ta "New children get P_PROTECTED"
+.It Dv "P2_NOTRACE" Ta No "0x00000002" Ta "No" Xr ptrace 2 attach or coredumps
+.It Dv "P2_NOTRACE_EXEC" Ta No "0x00000004" Ta Keep P2_NOPTRACE on Xr execve 2
+.It Dv "P2_AST_SU" Ta No "0x00000008" Ta "Handles SU ast for kthreads"
+.It Dv "P2_PTRACE_FSTP" Ta No "0x00000010" Ta "SIGSTOP from PT_ATTACH not yet handled"
+.It Dv "P2_TRAPCAP" Ta No "0x00000020" Ta "SIGTRAP on ENOTCAPABLE"
+.It Dv "P2_ASLR_ENABLE" Ta No "0x00000040" Ta "Force enable ASLR"
+.It Dv "P2_ASLR_DISABLE" Ta No "0x00000080" Ta "Force disable ASLR"
+.It Dv "P2_ASLR_IGNSTART" Ta No "0x00000100" Ta "Enable ASLR to consume sbrk area"
+.It Dv "P2_PROTMAX_ENABLE" Ta No "0x00000200" Ta "Force enable implied PROT_MAX"
+.It Dv "P2_PROTMAX_DISABLE" Ta No "0x00000400" Ta "Force disable implied PROT_MAX"
+.It Dv "P2_STKGAP_DISABLE" Ta No "0x00000800" Ta "Disable stack gap for MAP_STACK"
+.It Dv "P2_STKGAP_DISABLE_EXEC" Ta No "0x00001000" Ta "Stack gap disabled after exec"
+.It Dv "P2_ITSTOPPED" Ta No "0x00002000" Ta "itimers stopped (as part of process stop)"
+.It Dv "P2_PTRACEREQ" Ta No "0x00004000" Ta "Active ptrace req"
+.It Dv "P2_NO_NEW_PRIVS" Ta No "0x00008000" Ta "Ignore setuid on exec"
+.It Dv "P2_WXORX_DISABLE" Ta No "0x00010000" Ta "WX mappings enabled"
+.It Dv "P2_WXORX_ENABLE_EXEC" Ta No "0x00020000" Ta "WxorX enabled after exec"
+.It Dv "P2_WEXIT" Ta No "0x00040000" Ta "Internal exit early state"
+.It Dv "P2_REAPKILLED" Ta No "0x00080000" Ta "REAP_KILL pass handled the process"
+.It Dv "P2_MEMBAR_PRIVE" Ta No "0x00100000" Ta "membarrier private expedited registered"
+.It Dv "P2_MEMBAR_PRIVE_SYNCORE" Ta No "0x00200000" Ta "membarrier private expedited sync core registered"
+.It Dv "P2_MEMBAR_GLOBE" Ta No "0x00400000" Ta "membar global expedited registered"
+.El
+.It Cm label
+The MAC label of the process.
+.It Cm lim
+The soft limit on memory used, specified via a call to
+.Xr setrlimit 2 .
+.It Cm lstart
+The exact time the command started, using the
+.Ql %c
+format described in
+.Xr strftime 3 .
+.It Cm lockname
+The name of the lock that the process is currently blocked on.
+If the name is invalid or unknown, then
+.Dq ???\&
+is displayed.
+.It Cm logname
+The login name associated with the session the process is in (see
+.Xr getlogin 2 ) .
+.It Cm mwchan
+The event name if the process is blocked normally, or the lock name if
+the process is blocked on a lock.
+See the wchan and lockname keywords
+for details.
+.It Cm nice
+The process scheduling increment (see
+.Xr setpriority 2 ) .
+.It Cm rss
+the real memory (resident set) size of the process in KiB.
+.It Cm start
+The time the command started.
+If the command started less than 24 hours ago, the start time is
+displayed using the
+.Dq Li %H:%M
+format described in
+.Xr strftime 3 .
+If the command started less than 7 days ago, the start time is
+displayed using the
+.Dq Li %a%H
+format.
+Otherwise, the start time is displayed using the
+.Dq Li %e%b%y
+format.
+.It Cm sig
+The bitmask of signals pending in the process queue if the
+.Fl H
+option has not been specified, else the per-thread queue of pending signals.
+.It Cm state
+The state is given by a sequence of characters, for example,
+.Dq Li RWNA .
+The first character indicates the run state of the process:
+.Pp
+.Bl -tag -width indent -compact
+.It Li D
+Marks a process in disk (or other short term, uninterruptible) wait.
+.It Li I
+Marks a process that is idle (sleeping for longer than about 20 seconds).
+.It Li L
+Marks a process that is waiting to acquire a lock.
+.It Li R
+Marks a runnable process.
+.It Li S
+Marks a process that is sleeping for less than about 20 seconds.
+.It Li T
+Marks a stopped process.
+.It Li W
+Marks an idle interrupt thread.
+.It Li Z
+Marks a dead process (a
+.Dq zombie ) .
+.El
+.Pp
+Additional characters after these, if any, indicate additional state
+information:
+.Pp
+.Bl -tag -width indent -compact
+.It Li +
+The process is in the foreground process group of its control terminal.
+.It Li <
+The process has raised CPU scheduling priority.
+.It Li C
+The process is in
+.Xr capsicum 4
+capability mode.
+.It Li E
+The process is trying to exit.
+.It Li J
+Marks a process which is in
+.Xr jail 2 .
+The hostname of the prison can be found in
+.Pa /proc/ Ns Ao Ar pid Ac Ns Pa /status .
+.It Li L
+The process has pages locked in core (for example, for raw I/O).
+.It Li N
+The process has reduced CPU scheduling priority (see
+.Xr setpriority 2 ) .
+.It Li s
+The process is a session leader.
+.It Li V
+The process' parent is suspended during a
+.Xr vfork 2 ,
+waiting for the process to exec or exit.
+.It Li X
+The process is being traced or debugged.
+.El
+.It Cm tt
+An abbreviation for the pathname of the controlling terminal, if any.
+The abbreviation consists of the three letters following
+.Pa /dev/tty ,
+or, for pseudo-terminals, the corresponding entry in
+.Pa /dev/pts .
+This is followed by a
+.Ql -
+if the process can no longer reach that
+controlling terminal (i.e., it has been revoked).
+A
+.Ql -
+without a preceding two letter abbreviation or pseudo-terminal device number
+indicates a process which never had a controlling terminal.
+The full pathname of the controlling terminal is available via the
+.Cm tty
+keyword.
+.It Cm wchan
+The event (an address in the system) on which a process waits.
+When printed numerically, the initial part of the address is
+trimmed off and the result is printed in hex, for example, 0x80324000 prints
+as 324000.
+.El
+.Sh ENVIRONMENT
+The following environment variables affect the execution of
+.Nm :
+.Bl -tag -width ".Ev COLUMNS"
+.It Ev COLUMNS
+If set, specifies the user's preferred output width in column positions.
+Only affects the traditional text style output.
+Please see the preamble of this manual page on how the final output width is
+determined.
+.El
+.Sh FILES
+.Bl -tag -width ".Pa /boot/kernel/kernel" -compact
+.It Pa /boot/kernel/kernel
+default system namelist
+.El
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+Display information on all system processes:
+.Pp
+.Dl $ ps -auxw
+.Sh SEE ALSO
+.Xr kill 1 ,
+.Xr pgrep 1 ,
+.Xr pkill 1 ,
+.Xr procstat 1 ,
+.Xr w 1 ,
+.Xr kvm 3 ,
+.Xr libxo 3 ,
+.Xr strftime 3 ,
+.Xr xo_options 7 ,
+.Xr mac 4 ,
+.Xr procfs 4 ,
+.Xr pstat 8 ,
+.Xr sysctl 8 ,
+.Xr mutex 9
+.Sh STANDARDS
+For historical reasons, the
+.Nm
+utility under
+.Fx
+supports a different set of options from what is described by
+.St -p1003.1-2024
+and what is supported on
+.No non- Ns Bx
+operating systems.
+.Pp
+In particular, and contrary to this implementation, POSIX specifies that option
+.Fl d
+should serve to select all processes except session leaders, option
+.Fl e
+to select all processes
+.Po
+equivalently to
+.Fl A
+.Pc ,
+and option
+.Fl u
+to select processes by effective user ID.
+.Pp
+However, options
+.Fl A , a , G , l , o , p , U ,
+and
+.Fl t
+behave as prescribed by
+.St -p1003.1-2024 .
+Options
+.Fl f
+and
+.Fl w
+currently do not, but may be changed to in the future.
+.Pp
+POSIX's option
+.Fl g ,
+to select processes having the specified processes as their session leader, is
+not implemented.
+However, other UNIX systems that provide this functionality do so via option
+.Fl s
+instead, reserving
+.Fl g
+to query by group leaders.
+.Sh HISTORY
+The
+.Nm
+command appeared in
+.At v3
+in section 8 of the manual.
+.Sh BUGS
+Since
+.Nm
+cannot run faster than the system and is run as any other scheduled
+process, the information it displays can never be exact.
+.Pp
+.Nm ps
+currently does not correctly limit the ouput width, and in most cases does not
+limit it at all when it should.
+Regardless of the target width, requested columns are always all printed and
+with widths allowing to entirely print their longest values, except for columns
+with keyword
+.Cm command
+or
+.Cm args
+that are not last in the display
+.Pq they are truncated to 16 bytes ,
+and for the last column in the display if its keyword requests textual
+information of variable length, such as the
+.Cm command , jail ,
+and
+.Cm user
+keywords do.
+This considerably limits the effects and usefulness of the terminal width on the
+output, and consequently that of the
+.Ev COLUMNS
+environment variable and the
+.Fl w
+option
+.Pq if specified only once .
+.Pp
+The
+.Nm
+utility does not correctly display argument lists containing multibyte
+characters.
diff --git a/corebinutils/ps/ps.c b/corebinutils/ps/ps.c
new file mode 100644
index 0000000000..9c910f2a19
--- /dev/null
+++ b/corebinutils/ps/ps.c
@@ -0,0 +1,638 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1990, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ * Copyright (c) 2025 The FreeBSD Foundation
+ * Copyright (c) 2026 Project Tick. All rights reserved.
+ *
+ * Portions of this software were developed by Olivier Certner
+ * <olce@FreeBSD.org> at Kumacom SARL under sponsorship from the FreeBSD
+ * Foundation.
+ *
+ * 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.
+ * ------+---------+---------+-------- + --------+---------+---------+---------*
+ * Copyright (c) 2004 - Garance Alistair Drosehn <gad@FreeBSD.org>.
+ * All rights reserved.
+ *
+ * Significant modifications made to bring `ps' options somewhat closer
+ * to the standard for `ps' as described in SingleUnixSpec-v3.
+ * ------+---------+---------+-------- + --------+---------+---------+---------*
+ */
+
+#include <sys/param.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <sys/sysmacros.h>
+
+#include <ctype.h>
+#include <dirent.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <grp.h>
+#include <limits.h>
+#include <locale.h>
+#include <paths.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+
+#include "ps.h"
+
+/* Globals */
+int cflag; /* -c */
+int eval; /* Exit value */
+time_t now; /* Current time(3) value */
+int rawcpu; /* -C */
+int sumrusage; /* -S */
+int termwidth; /* Width of the screen (0 == infinity). */
+int showthreads; /* will threads be shown? */
+int hlines; /* repeat headers every N lines */
+
+struct velisthead varlist = STAILQ_HEAD_INITIALIZER(varlist);
+struct velisthead Ovarlist = STAILQ_HEAD_INITIALIZER(Ovarlist);
+
+static int needcomm, needenv, needuser, optfatal;
+static enum sort { DEFAULT, SORTMEM, SORTCPU } sortby = DEFAULT;
+
+static long clk_tck;
+static double system_uptime;
+
+struct listinfo {
+ int count;
+ int maxcount;
+ int elemsize;
+ int (*addelem)(struct listinfo *, const char *);
+ const char *lname;
+ void *l;
+};
+
+/* Forward declarations */
+static int addelem_gid(struct listinfo *, const char *);
+static int addelem_pid(struct listinfo *, const char *);
+static int addelem_tty(struct listinfo *, const char *);
+static int addelem_uid(struct listinfo *, const char *);
+static void add_list(struct listinfo *, const char *);
+static int pscomp(const void *, const void *);
+static void scan_vars(void);
+static void usage(void) __attribute__((__noreturn__));
+static int scan_processes(KINFO **kinfop, int *nentries);
+static double get_system_uptime(void);
+static char *kludge_oldps_options(const char *, char *, const char *);
+
+static const char dfmt[] = "pid,tt,state,time,command";
+static const char jfmt[] = "user,pid,ppid,pgid,sid,jobc,state,tt,time,command";
+static const char lfmt[] = "uid,pid,ppid,cpu,pri,nice,vsz,rss,mwchan,state,tt,time,command";
+static const char ufmt[] = "user,pid,%cpu,%mem,vsz,rss,tt,state,start,time,command";
+static const char vfmt[] = "pid,state,time,sl,vsz,rss,lim,tsiz,%cpu,%mem,command";
+
+#define PS_ARGS "AaCcD:defG:gHhjJ:LlM:mN:O:o:p:rSTt:U:uvwXxZ"
+
+int
+main(int argc, char *argv[])
+{
+ struct listinfo gidlist, pidlist, ruidlist, sesslist, ttylist, uidlist, pgrplist;
+ KINFO *kinfo = NULL;
+ struct varent *vent;
+ struct winsize ws;
+ char *cols;
+ int all, ch, i, nentries, nkept, nselectors, wflag, xkeep, xkeep_implied;
+
+ setlocale(LC_ALL, "");
+ time(&now);
+
+ clk_tck = sysconf(_SC_CLK_TCK);
+ system_uptime = get_system_uptime();
+
+ if ((cols = getenv("COLUMNS")) != NULL && *cols != '\0')
+ termwidth = atoi(cols);
+ else if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
+ termwidth = ws.ws_col - 1;
+ if (ws.ws_row > 0) hlines = ws.ws_row - 1;
+ } else {
+ termwidth = 79;
+ hlines = 22;
+ }
+ /* -h default is active only if -h is passed, so we use hlines as the value if hflag set */
+ int hflag = 0;
+
+ if (argc > 1)
+ argv[1] = kludge_oldps_options(PS_ARGS, argv[1], argv[2]);
+
+ all = nselectors = optfatal = wflag = xkeep_implied = 0;
+ xkeep = -1;
+
+ memset(&gidlist, 0, sizeof(gidlist)); gidlist.addelem = addelem_gid; gidlist.elemsize = sizeof(gid_t);
+ memset(&pidlist, 0, sizeof(pidlist)); pidlist.addelem = addelem_pid; pidlist.elemsize = sizeof(pid_t);
+ memset(&ruidlist, 0, sizeof(ruidlist)); ruidlist.addelem = addelem_uid; ruidlist.elemsize = sizeof(uid_t);
+ memset(&sesslist, 0, sizeof(sesslist)); sesslist.addelem = addelem_pid; sesslist.elemsize = sizeof(pid_t);
+ memset(&ttylist, 0, sizeof(ttylist)); ttylist.addelem = addelem_tty; ttylist.elemsize = sizeof(dev_t);
+ memset(&uidlist, 0, sizeof(uidlist)); uidlist.addelem = addelem_uid; uidlist.elemsize = sizeof(uid_t);
+ memset(&pgrplist, 0, sizeof(pgrplist)); pgrplist.addelem = addelem_pid; pgrplist.elemsize = sizeof(pid_t);
+
+ int _fmt = 0;
+ while ((ch = getopt(argc, argv, PS_ARGS)) != -1)
+ switch (ch) {
+ case 'A': all = xkeep = 1; break;
+ case 'a': all = 1; break;
+ case 'C': rawcpu = 1; break;
+ case 'c': cflag = 1; break;
+ case 'e': needenv = 1; break;
+ case 'f': /* default in FreeBSD */ break;
+ case 'G': add_list(&gidlist, optarg); xkeep_implied = 1; nselectors++; break;
+ case 'g': /* BSD compat: leaders (no-op on Linux for now) */ break;
+ case 'H': showthreads = KERN_PROC_INC_THREAD; break;
+ case 'h': hflag = 1; break;
+ case 'j': parsefmt(jfmt, &varlist, 0); _fmt = 1; break;
+ case 'L': showkey(); exit(0);
+ case 'l': parsefmt(lfmt, &varlist, 0); _fmt = 1; break;
+ case 'M': case 'N': warnx("-%c not supported on Linux", ch); break;
+ case 'm': sortby = SORTMEM; break;
+ case 'O': parsefmt(optarg, &Ovarlist, 1); break;
+ case 'o': parsefmt(optarg, &varlist, 1); _fmt = 1; break;
+ case 'p': add_list(&pidlist, optarg); nselectors++; break;
+ case 'r': sortby = SORTCPU; break;
+ case 'S': sumrusage = 1; break;
+ case 'T': add_list(&ttylist, ttyname(STDIN_FILENO)); xkeep_implied = 1; nselectors++; break;
+ case 't': add_list(&ttylist, optarg); xkeep_implied = 1; nselectors++; break;
+ case 'U': add_list(&ruidlist, optarg); xkeep_implied = 1; nselectors++; break;
+ case 'u': parsefmt(ufmt, &varlist, 0); sortby = SORTCPU; _fmt = 1; break;
+ case 'v': parsefmt(vfmt, &varlist, 0); sortby = SORTMEM; _fmt = 1; break;
+ case 'w': if (wflag) termwidth = 0; else if (termwidth < 131) termwidth = 131; wflag++; break;
+ case 'X': xkeep = 0; break;
+ case 'x': xkeep = 1; break;
+ case 'Z': parsefmt("label", &varlist, 0); break;
+ default: usage();
+ }
+
+ argc -= optind; argv += optind;
+ while (*argv && isdigit(**argv)) { add_list(&pidlist, *argv); nselectors++; argv++; }
+ if (*argv) errx(1, "illegal argument: %s", *argv);
+ if (optfatal) exit(1);
+ if (xkeep < 0) xkeep = xkeep_implied;
+ if (!hflag) hlines = 0;
+
+ if (!_fmt) parsefmt(dfmt, &varlist, 0);
+ if (!STAILQ_EMPTY(&Ovarlist)) {
+ /* Simple join for now */
+ STAILQ_CONCAT(&varlist, &Ovarlist);
+ }
+
+ if (STAILQ_EMPTY(&varlist)) {
+ warnx("no keywords specified");
+ showkey();
+ exit(1);
+ }
+
+ scan_vars();
+
+ if (all) nselectors = 0;
+ else if (nselectors == 0) {
+ uid_t me = geteuid();
+ uidlist.l = realloc(uidlist.l, sizeof(uid_t));
+ ((uid_t*)uidlist.l)[0] = me;
+ uidlist.count = 1;
+ nselectors = 1;
+ }
+
+ if (scan_processes(&kinfo, &nentries) < 0) err(1, "scan_processes");
+
+ for (i = 0; i < nentries; i++)
+ kinfo[i].ki_pcpu = getpcpu(&kinfo[i]);
+
+ nkept = 0;
+ KINFO *kept = malloc(nentries * sizeof(KINFO));
+ if (!kept) err(1, "malloc");
+
+ for (i = 0; i < nentries; i++) {
+ struct kinfo_proc *kp = kinfo[i].ki_p;
+ int match = 0;
+ if (pidlist.count > 0) {
+ for (int j = 0; j < pidlist.count; j++) {
+ // printf("Checking pid %d against %d\n", kp->ki_pid, ((pid_t*)pidlist.l)[j]);
+ if (kp->ki_pid == ((pid_t*)pidlist.l)[j]) { match = 1; break; }
+ }
+ }
+ if (!match && xkeep == 0 && (kp->ki_tdev == (dev_t)-1)) continue;
+ if (!match && nselectors > 0) {
+ if (gidlist.count > 0) {
+ for (int j = 0; j < gidlist.count; j++)
+ if (kp->ki_groups[0] == ((gid_t*)gidlist.l)[j]) { match = 1; break; }
+ }
+ if (!match && ruidlist.count > 0) {
+ for (int j = 0; j < ruidlist.count; j++)
+ if (kp->ki_ruid == ((uid_t*)ruidlist.l)[j]) { match = 1; break; }
+ }
+ if (!match && uidlist.count > 0) {
+ for (int j = 0; j < uidlist.count; j++)
+ if (kp->ki_uid == ((uid_t*)uidlist.l)[j]) { match = 1; break; }
+ }
+ if (!match && ttylist.count > 0) {
+ for (int j = 0; j < ttylist.count; j++)
+ if (kp->ki_tdev == ((dev_t*)ttylist.l)[j]) { match = 1; break; }
+ }
+ if (!match) continue;
+ } else if (!match && xkeep == 0 && (kp->ki_tdev == (dev_t)-1)) continue;
+
+ kept[nkept++] = kinfo[i];
+ }
+
+ if (nkept == 0) {
+ printheader();
+ exit(1);
+ }
+
+ qsort(kept, nkept, sizeof(KINFO), pscomp);
+
+ printheader();
+ for (i = 0; i < nkept; i++) {
+ if (hlines > 0 && i > 0 && i % hlines == 0) printheader();
+ int linelen = 0;
+ STAILQ_FOREACH(vent, &varlist, next_ve) {
+ char *str = vent->var->oproc(&kept[i], vent);
+ if (!str) str = strdup("-");
+ int width = vent->width;
+ if (STAILQ_NEXT(vent, next_ve) == NULL) {
+ if (termwidth > 0 && linelen + (int)strlen(str) > termwidth) {
+ int avail = termwidth - linelen;
+ if (avail > 0) str[avail] = '\0';
+ }
+ printf("%s", str);
+ } else {
+ if (vent->var->flag & LJUST) printf("%-*s ", width, str);
+ else printf("%*s ", width, str);
+ linelen += width + 1;
+ }
+ free(str);
+ }
+ printf("\n");
+ }
+
+ for (i = 0; i < nentries; i++) {
+ KINFO_STR *ks;
+ while (!STAILQ_EMPTY(&kinfo[i].ki_ks)) {
+ ks = STAILQ_FIRST(&kinfo[i].ki_ks);
+ STAILQ_REMOVE_HEAD(&kinfo[i].ki_ks, ks_next);
+ free(ks->ks_str);
+ free(ks);
+ }
+ free(kinfo[i].ki_p);
+ free(kinfo[i].ki_args);
+ free(kinfo[i].ki_env);
+ }
+ free(kinfo);
+ free(kept);
+ free_devnames();
+
+ return 0;
+}
+
+static void
+scan_vars(void)
+{
+ struct varent *vent;
+ STAILQ_FOREACH(vent, &varlist, next_ve) {
+ if (vent->var->flag & COMM) needcomm = 1;
+ if (vent->var->flag & USER) needuser = 1;
+ }
+}
+
+static int
+addelem_pid(struct listinfo *inf, const char *arg)
+{
+ long v = strtol(arg, NULL, 10);
+ inf->l = realloc(inf->l, (inf->count + 1) * sizeof(pid_t));
+ ((pid_t*)inf->l)[inf->count++] = (pid_t)v;
+ return 0;
+}
+
+static int
+addelem_uid(struct listinfo *inf, const char *arg)
+{
+ struct passwd *pw = getpwnam(arg);
+ uid_t uid = pw ? pw->pw_uid : (uid_t)atoi(arg);
+ inf->l = realloc(inf->l, (inf->count + 1) * sizeof(uid_t));
+ ((uid_t*)inf->l)[inf->count++] = uid;
+ return 0;
+}
+
+static int
+addelem_gid(struct listinfo *inf, const char *arg)
+{
+ struct group *gr = getgrnam(arg);
+ gid_t gid = gr ? gr->gr_gid : (gid_t)atoi(arg);
+ inf->l = realloc(inf->l, (inf->count + 1) * sizeof(gid_t));
+ ((gid_t*)inf->l)[inf->count++] = gid;
+ return 0;
+}
+
+static int
+addelem_tty(struct listinfo *inf, const char *arg)
+{
+ char path[PATH_MAX];
+ struct stat st;
+ if (arg[0] != '/') snprintf(path, sizeof(path), "/dev/%s", arg);
+ else snprintf(path, sizeof(path), "%s", arg);
+ if (stat(path, &st) == 0 && S_ISCHR(st.st_mode)) {
+ inf->l = realloc(inf->l, (inf->count + 1) * sizeof(dev_t));
+ ((dev_t*)inf->l)[inf->count++] = st.st_rdev;
+ return 0;
+ }
+ return -1;
+}
+
+static void
+add_list(struct listinfo *inf, const char *arg)
+{
+ char *copy = strdup(arg);
+ char *p = copy, *token;
+ while ((token = strsep(&p, " \t,")) != NULL) {
+ if (*token == '\0') continue;
+ if (inf->addelem(inf, token) < 0) optfatal = 1;
+ }
+ free(copy);
+}
+
+static int
+pscomp(const void *a, const void *b)
+{
+ const KINFO *ka = a, *kb = b;
+ if (sortby == SORTMEM) {
+ if (ka->ki_p->ki_rssize < kb->ki_p->ki_rssize) return 1;
+ if (ka->ki_p->ki_rssize > kb->ki_p->ki_rssize) return -1;
+ return 0;
+ }
+ if (sortby == SORTCPU) {
+ if (ka->ki_pcpu < kb->ki_pcpu) return 1;
+ if (ka->ki_pcpu > kb->ki_pcpu) return -1;
+ return 0;
+ }
+ if (ka->ki_p->ki_pid < kb->ki_p->ki_pid) return -1;
+ if (ka->ki_p->ki_pid > kb->ki_p->ki_pid) return 1;
+ return 0;
+}
+
+static double
+get_system_uptime(void)
+{
+ FILE *fp = fopen("/proc/uptime", "r");
+ double uptime = 0.0;
+ if (fp) {
+ if (fscanf(fp, "%lf", &uptime) != 1) uptime = 0.0;
+ fclose(fp);
+ }
+ return uptime;
+}
+
+static long long
+safe_strtoll(const char *s)
+{
+ char *end;
+ if (s == NULL || *s == '\0') return 0;
+ long long val = strtoll(s, &end, 10);
+ if (end == s) return 0;
+ return val;
+}
+
+static unsigned long
+safe_strtoul(const char *s)
+{
+ char *end;
+ if (s == NULL || *s == '\0') return 0;
+ unsigned long val = strtoul(s, &end, 10);
+ if (end == s) return 0;
+ return val;
+}
+
+/* Linux-specific /proc scanning */
+
+static char*
+read_file(const char *path)
+{
+ int fd = open(path, O_RDONLY);
+ if (fd < 0) return NULL;
+ char *buf = malloc(8192);
+ if (!buf) { close(fd); return NULL; }
+ ssize_t n = read(fd, buf, 8191);
+ close(fd);
+ if (n <= 0) { free(buf); return NULL; }
+ buf[n] = '\0';
+ return buf;
+}
+
+static int
+read_proc_stat(pid_t pid, struct kinfo_proc *ki)
+{
+ char path[64], *buf;
+ snprintf(path, sizeof(path), "/proc/%d/stat", pid);
+ buf = read_file(path);
+ if (!buf) return -1;
+
+ char *p = strrchr(buf, ')');
+ if (!p) { free(buf); return -1; }
+
+ /* Field 1: pid (already known) */
+ /* Field 2: comm (already got p) */
+ char *comm_start = strchr(buf, '(');
+ if (comm_start) {
+ size_t len = p - (comm_start + 1);
+ if (len >= sizeof(ki->ki_comm)) len = sizeof(ki->ki_comm) - 1;
+ memcpy(ki->ki_comm, comm_start + 1, len);
+ ki->ki_comm[len] = '\0';
+ }
+
+ p += 2; /* Skip ") " */
+ char *tokens[50];
+ int ntok = 0;
+ char *sp;
+ while ((sp = strsep(&p, " ")) != NULL && ntok < 50) {
+ tokens[ntok++] = sp;
+ }
+
+ if (ntok < 20) { free(buf); return -1; }
+
+ ki->ki_stat = (tokens[0] && tokens[0][0]) ? tokens[0][0] : '?';
+ ki->ki_ppid = (int)safe_strtoll(tokens[1]);
+ ki->ki_pgid = (int)safe_strtoll(tokens[2]);
+ ki->ki_sid = (int)safe_strtoll(tokens[3]);
+
+ unsigned int tty_nr = (unsigned int)safe_strtoul(tokens[4]);
+ if (tty_nr == 0) {
+ ki->ki_tdev = (dev_t)-1;
+ } else {
+ unsigned int maj = (tty_nr >> 8) & 0xFFF;
+ unsigned int min = (tty_nr & 0xFF) | ((tty_nr >> 12) & 0xFFF00);
+ ki->ki_tdev = makedev(maj, min);
+ }
+
+ ki->ki_tpgid = tokens[5] ? (int)safe_strtoll(tokens[5]) : -1;
+ ki->ki_flag = (long)safe_strtoll(tokens[6]);
+
+ long long utime = safe_strtoll(tokens[11]);
+ long long stime = safe_strtoll(tokens[12]);
+ ki->ki_runtime = (uint64_t)((utime + stime) * 1000000 / clk_tck);
+
+ ki->ki_pri = (int)safe_strtoll(tokens[15]);
+ ki->ki_nice = (int)safe_strtoll(tokens[16]);
+ ki->ki_numthreads = (ntok > 17) ? (int)safe_strtoll(tokens[17]) : 1;
+ if (ki->ki_numthreads <= 0) ki->ki_numthreads = 1;
+
+ if (ntok > 19 && tokens[19]) {
+ double boot_time = (double)now - system_uptime;
+ double start_ticks = (double)safe_strtoll(tokens[19]);
+ ki->ki_start.tv_sec = (time_t)(boot_time + (start_ticks / clk_tck));
+ ki->ki_start.tv_usec = 0;
+ } else {
+ ki->ki_start.tv_sec = now;
+ ki->ki_start.tv_usec = 0;
+ }
+
+ ki->ki_size = (ntok > 20) ? (uint64_t)safe_strtoll(tokens[20]) : 0;
+ ki->ki_rssize = (ntok > 21) ? (uint64_t)safe_strtoll(tokens[21]) * (getpagesize() / 1024) : 0;
+
+ free(buf);
+ return 0;
+}
+
+static int
+read_proc_status(pid_t pid, struct kinfo_proc *ki)
+{
+ char path[64];
+ snprintf(path, sizeof(path), "/proc/%d/status", pid);
+ FILE *fp = fopen(path, "r");
+ if (!fp) return -1;
+ char line[256];
+ while (fgets(line, sizeof(line), fp)) {
+ if (strncmp(line, "Uid:", 4) == 0) {
+ sscanf(line + 4, "%u %u %u", &ki->ki_ruid, &ki->ki_uid, &ki->ki_svuid);
+ } else if (strncmp(line, "Gid:", 4) == 0) {
+ sscanf(line + 4, "%u %u %u", &ki->ki_rgid, &ki->ki_groups[0], &ki->ki_svgid);
+ }
+ }
+ fclose(fp);
+ return 0;
+}
+
+static char*
+read_proc_cmdline(pid_t pid)
+{
+ char path[64];
+ snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
+ int fd = open(path, O_RDONLY);
+ if (fd < 0) return NULL;
+ char *buf = malloc(4096);
+ if (!buf) { close(fd); return NULL; }
+ ssize_t n = read(fd, buf, 4095);
+ close(fd);
+ if (n <= 0) { free(buf); return NULL; }
+ for (int i = 0; i < n - 1; i++) if (buf[i] == '\0') buf[i] = ' ';
+ buf[n] = '\0';
+ return buf;
+}
+
+static char*
+read_proc_environ(pid_t pid)
+{
+ char path[64];
+ snprintf(path, sizeof(path), "/proc/%d/environ", pid);
+ int fd = open(path, O_RDONLY);
+ if (fd < 0) return NULL;
+ char *buf = malloc(4096);
+ if (!buf) { close(fd); return NULL; }
+ ssize_t n = read(fd, buf, 4095);
+ close(fd);
+ if (n <= 0) { free(buf); return NULL; }
+ for (int i = 0; i < n - 1; i++) if (buf[i] == '\0') buf[i] = ' ';
+ buf[n] = '\0';
+ return buf;
+}
+
+static int
+scan_processes(KINFO **kinfop, int *nentries)
+{
+ DIR *dir = opendir("/proc");
+ if (!dir) return -1;
+ struct dirent *ent;
+ int count = 0, cap = 128;
+ KINFO *k = malloc(cap * sizeof(KINFO));
+
+ while ((ent = readdir(dir))) {
+ if (!isdigit(ent->d_name[0])) continue;
+ pid_t pid = atoi(ent->d_name);
+ struct kinfo_proc *kp = calloc(1, sizeof(struct kinfo_proc));
+ kp->ki_pid = pid;
+ if (read_proc_stat(pid, kp) < 0 || read_proc_status(pid, kp) < 0) {
+ free(kp);
+ continue;
+ }
+ // printf("Scanned pid %d: comm=%s ppid=%d\n", kp->ki_pid, kp->ki_comm, kp->ki_ppid);
+
+ if (count >= cap) {
+ cap *= 2;
+ k = realloc(k, cap * sizeof(KINFO));
+ }
+ k[count].ki_p = kp;
+ k[count].ki_valid = 1;
+ k[count].ki_args = needcomm ? read_proc_cmdline(pid) : NULL;
+ if (!k[count].ki_args) k[count].ki_args = strdup(kp->ki_comm);
+ k[count].ki_env = (needenv) ? read_proc_environ(pid) : NULL;
+ k[count].ki_pcpu = 0; /* filled later if needed */
+ k[count].ki_memsize = kp->ki_rssize * getpagesize();
+ k[count].ki_d.prefix = NULL;
+ STAILQ_INIT(&k[count].ki_ks);
+ count++;
+ }
+ closedir(dir);
+ *kinfop = k;
+ *nentries = count;
+ return 0;
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: ps [%s]\n", PS_ARGS);
+ exit(1);
+}
+
+static char *
+kludge_oldps_options(const char *optstring, char *arg, const char *nextarg)
+{
+ /* Simple version of BSD kludge: if first arg doesn't start with '-', prepend one */
+ if (arg && arg[0] != '-') {
+ char *newarg = malloc(strlen(arg) + 2);
+ newarg[0] = '-';
+ strcpy(newarg + 1, arg);
+ return newarg;
+ }
+ return arg;
+}
diff --git a/corebinutils/ps/ps.h b/corebinutils/ps/ps.h
new file mode 100644
index 0000000000..fd18e6ef8f
--- /dev/null
+++ b/corebinutils/ps/ps.h
@@ -0,0 +1,205 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1990, 1993
+ * The Regents of the University of California. All rights reserved.
+ * 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.
+ *
+ * Linux-native port: replaces BSD kinfo_proc with a Linux-compatible equivalent
+ * that can be filled from /proc.
+ */
+
+#ifndef _PS_H_
+#define _PS_H_
+
+#include <sys/types.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+/* Minimal STAILQ for musl-gcc compatibility */
+#define STAILQ_HEAD(name, type) \
+struct name { struct type *stqh_first; struct type **stqh_last; }
+#define STAILQ_HEAD_INITIALIZER(head) { NULL, &(head).stqh_first }
+#define STAILQ_ENTRY(type) \
+struct { struct type *stqe_next; }
+#define STAILQ_FIRST(head) ((head)->stqh_first)
+#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next)
+#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL)
+#define STAILQ_INIT(head) do { (head)->stqh_first = NULL; (head)->stqh_last = &(head)->stqh_first; } while (0)
+#define STAILQ_INSERT_TAIL(head, elm, field) do { \
+ (elm)->field.stqe_next = NULL; \
+ *(head)->stqh_last = (elm); \
+ (head)->stqh_last = &(elm)->field.stqe_next; \
+} while (0)
+#define STAILQ_FOREACH(var, head, field) \
+ for ((var) = STAILQ_FIRST(head); (var); (var) = STAILQ_NEXT(var, field))
+#define STAILQ_CONCAT(head1, head2) do { \
+ if (!STAILQ_EMPTY(head2)) { \
+ *(head1)->stqh_last = (head2)->stqh_first; \
+ (head1)->stqh_last = (head2)->stqh_last; \
+ STAILQ_INIT(head2); \
+ } \
+} while (0)
+#define STAILQ_REMOVE_HEAD(head, field) do { \
+ if (((head)->stqh_first = (head)->stqh_first->field.stqe_next) == NULL) \
+ (head)->stqh_last = &(head)->stqh_first; \
+} while (0)
+
+#define UNLIMITED 0
+
+#ifndef __unused
+#define __unused __attribute__((__unused__))
+#endif
+
+#define COMMLEN 256
+#define WMESGLEN 64
+#define KI_NGROUPS 16
+
+/* Linux-port version of kinfo_proc to store data from /proc */
+struct kinfo_proc {
+ pid_t ki_pid;
+ pid_t ki_ppid;
+ pid_t ki_pgid;
+ pid_t ki_sid;
+ dev_t ki_tdev;
+ pid_t ki_tpgid;
+ uid_t ki_uid;
+ uid_t ki_ruid;
+ uid_t ki_svuid;
+ gid_t ki_groups[KI_NGROUPS];
+ gid_t ki_rgid;
+ gid_t ki_svgid;
+ char ki_comm[COMMLEN];
+ char ki_tdname[64];
+ char ki_moretdname[64];
+ struct timeval ki_start;
+ uint64_t ki_runtime; // in microseconds
+ uint64_t ki_size; // VSZ in bytes
+ uint64_t ki_rssize; // RSS in pages
+ uint64_t ki_tsize; // Text size in pages (hard to get precise on Linux)
+ uint64_t ki_dsize; // Data size in pages
+ uint64_t ki_ssize; // Stack size in pages
+ int ki_nice;
+ int ki_pri_level;
+ int ki_pri_class;
+ char ki_stat; // BSD-like state character (S, R, T, Z, D)
+ long ki_flag; // Linux task flags
+ char ki_wmesg[WMESGLEN];
+ int ki_numthreads;
+ long ki_slptime; // time since last running (approximate)
+ int ki_oncpu;
+ int ki_lastcpu;
+ int ki_jid; // Always 0 on Linux
+ char ki_login[64]; // placeholder for logname
+ struct rusage ki_rusage; // approximate from /proc/pid/stat
+ struct timeval ki_childtime; // placeholder for sumrusage
+ struct timeval ki_childstime;
+ struct timeval ki_childutime;
+};
+
+/* Compatibility aliases for FreeBSD KVM-based fields used in print.c */
+#define ki_pri ki_pri_level
+
+typedef long fixpt_t;
+typedef uint64_t segsz_t;
+
+enum type { UNSPEC, CHAR, UCHAR, SHORT, USHORT, INT, UINT, LONG, ULONG, KPTR, PGTOK };
+
+typedef struct kinfo_str {
+ STAILQ_ENTRY(kinfo_str) ks_next;
+ char *ks_str; /* formatted string */
+} KINFO_STR;
+
+typedef struct kinfo {
+ struct kinfo_proc *ki_p; /* kinfo_proc structure */
+ char *ki_args; /* exec args (heap) */
+ char *ki_env; /* environment (heap) */
+ int ki_valid; /* 1 => data valid */
+ double ki_pcpu; /* calculated in main() */
+ segsz_t ki_memsize; /* calculated in main() */
+ union {
+ int level; /* used in descendant_sort() */
+ char *prefix; /* prefix string for tree-view */
+ } ki_d;
+ STAILQ_HEAD(, kinfo_str) ki_ks;
+} KINFO;
+
+struct var;
+typedef struct var VAR;
+
+typedef struct varent {
+ STAILQ_ENTRY(varent) next_ve;
+ const char *header;
+ const struct var *var;
+ u_int width;
+#define VE_KEEP (1 << 0)
+ uint16_t flags;
+} VARENT;
+
+STAILQ_HEAD(velisthead, varent);
+
+struct var {
+ const char *name;
+ union {
+ const char *aliased;
+ const VAR *final_kw;
+ };
+ const char *header;
+ const char *field;
+#define COMM 0x01
+#define LJUST 0x02
+#define USER 0x04
+#define INF127 0x10
+#define NOINHERIT 0x1000
+#define RESOLVING_ALIAS 0x10000
+#define RESOLVED_ALIAS 0x20000
+ u_int flag;
+ char *(*oproc)(struct kinfo *, struct varent *);
+ size_t off;
+ enum type type;
+ const char *fmt;
+};
+
+#define KERN_PROC_PROC 0
+#define KERN_PROC_THREAD 1
+#define KERN_PROC_INC_THREAD 2
+#define KERN_PROC_ALL 3
+
+/* Don't define these if they conflict with system headers */
+#ifndef NZERO
+#define NZERO 20
+#endif
+
+/* Linux PRI mapping */
+#define PUSER 0
+
+#include "extern.h"
+
+#endif /* _PS_H_ */
diff --git a/corebinutils/ps/tests/spin_helper.c b/corebinutils/ps/tests/spin_helper.c
new file mode 100644
index 0000000000..eef1d0e3b7
--- /dev/null
+++ b/corebinutils/ps/tests/spin_helper.c
@@ -0,0 +1,45 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <signal.h>
+#include <string.h>
+#include <sys/prctl.h>
+
+/*
+ * Small helper to create processes with predictable names/properties for ps tests.
+ */
+
+static void
+handle_sigterm(int sig)
+{
+ (void)sig;
+ exit(0);
+}
+
+int
+main(int argc, char **argv)
+{
+ if (argc < 2) {
+ fprintf(stderr, "usage: %s <name> [arg...]\n", argv[0]);
+ return (1);
+ }
+
+ /* Set name for /proc/self/comm (visible in ps -c) */
+ prctl(PR_SET_NAME, argv[1], 0, 0, 0);
+
+ signal(SIGTERM, handle_sigterm);
+
+ /* Busy wait in background to consume some CPU time if needed */
+ if (argc > 2 && strcmp(argv[2], "busy") == 0) {
+ while (1) {
+ /* Do nothing, just spin */
+ }
+ }
+
+ /* Normal idle wait */
+ while (1) {
+ pause();
+ }
+
+ return (0);
+}
diff --git a/corebinutils/ps/tests/test.sh b/corebinutils/ps/tests/test.sh
new file mode 100644
index 0000000000..004bd9f372
--- /dev/null
+++ b/corebinutils/ps/tests/test.sh
@@ -0,0 +1,150 @@
+#!/bin/sh
+
+# ps tests
+#
+# Assumptions:
+# - PS_BIN points to the newly built ps
+# - TEST_HELPER points to tests/spin_helper.c compiled into out/tests/spin_helper
+
+# Colors
+GREEN='\033[0;32m'
+RED='\033[0;31m'
+NC='\033[0m'
+
+fail() {
+ printf "${RED}FAIL: %s${NC}\n" "$1" >&2
+ exit 1
+}
+
+pass() {
+ printf "${GREEN}PASS: %s${NC}\n" "$1"
+}
+
+# Build a ps-specific helper in shared out/ roots to avoid name collisions
+# with other test suites (e.g. pkill also has a spin_helper).
+HELPER_DIR="$(dirname "$0")"
+OUT_DIR="$(dirname "${PS_BIN}")"
+HELPER_BIN="${OUT_DIR}/ps_spin_helper"
+HELPER_SRC="${HELPER_DIR}/spin_helper.c"
+CC_BIN="${CC:-cc}"
+
+if [ ! -x "${HELPER_BIN}" ] || [ "${HELPER_SRC}" -nt "${HELPER_BIN}" ]; then
+ "${CC_BIN}" -O2 "${HELPER_SRC}" -o "${HELPER_BIN}" || fail "failed to compile spin_helper"
+fi
+
+# 1. Basic sanity: run without arguments
+# Should output at least headers and some processes.
+${PS_BIN} > /tmp/ps_out || fail "ps failed to run"
+grep -q "PID" /tmp/ps_out || fail "ps output lacks PID header"
+grep -q "COMMAND" /tmp/ps_out || fail "ps output lacks COMMAND header"
+pass "Basic sanity (no args)"
+
+# 2. Process selection: -p
+# Start a helper, then find it with ps.
+${HELPER_BIN} test-ps-select &
+HELPER_PID=$!
+sleep 0.1
+${PS_BIN} -p ${HELPER_PID} > /tmp/ps_p_out || fail "ps -p failed"
+grep -q "${HELPER_PID}" /tmp/ps_p_out || fail "ps -p did not find helper pid ${HELPER_PID}"
+kill ${HELPER_PID}
+pass "Process selection (-p pid)"
+
+# 3. Format selection: -j
+${HELPER_BIN} test-ps-fmt-j &
+HELPER_PID=$!
+sleep 0.1
+${PS_BIN} -j -p ${HELPER_PID} > /tmp/ps_j_out || fail "ps -j failed"
+grep -q "USER" /tmp/ps_j_out || fail "ps -j output lacks USER header"
+grep -q "PPID" /tmp/ps_j_out || fail "ps -j output lacks PPID header"
+grep -q "SID" /tmp/ps_j_out || fail "ps -j output lacks SID header"
+kill ${HELPER_PID}
+pass "Format selection (-j)"
+
+# 4. Format selection: -u
+${HELPER_BIN} test-ps-fmt-u &
+HELPER_PID=$!
+sleep 0.1
+${PS_BIN} -u -p ${HELPER_PID} > /tmp/ps_u_out || fail "ps -u failed"
+grep -q "%CPU" /tmp/ps_u_out || fail "ps -u output lacks %CPU header"
+grep -q "%MEM" /tmp/ps_u_out || fail "ps -u output lacks %MEM header"
+grep -q "VSZ" /tmp/ps_u_out || fail "ps -u output lacks VSZ header"
+grep -q "RSS" /tmp/ps_u_out || fail "ps -u output lacks RSS header"
+kill ${HELPER_PID}
+pass "Format selection (-u)"
+
+# 5. Format selection: -o (custom)
+${HELPER_BIN} test-ps-fmt-o &
+HELPER_PID=$!
+sleep 0.1
+${PS_BIN} -o pid,comm,nice -p ${HELPER_PID} > /tmp/ps_o_out || fail "ps -o failed"
+grep -q "PID" /tmp/ps_o_out || fail "ps -o output lacks PID header"
+grep -q "COMMAND" /tmp/ps_o_out || fail "ps -o output lacks COMMAND header"
+grep -q "NI" /tmp/ps_o_out || fail "ps -o output lacks NI header"
+# First line is header, second line should contain the PID.
+grep -q "${HELPER_PID}" /tmp/ps_o_out || fail "ps -o did not find helper pid"
+kill ${HELPER_PID}
+pass "Format selection (-o pid,comm,nice)"
+
+# 6. Sorting: -m (by memory) and -r (by cpu)
+${PS_BIN} -m -A > /dev/null || fail "ps -m -A failed"
+${PS_BIN} -r -A > /dev/null || fail "ps -r -A failed"
+pass "Sorting (-m, -r)"
+
+# 7. Negative test: invalid pid
+${PS_BIN} -p 999999 > /tmp/ps_neg_out 2>&1
+# FreeBSD ps should just print header and exits 1 if no matches found?
+# Actually, standard behavior varies. Let's check status.
+EXIT_CODE=$?
+if [ ${EXIT_CODE} -ne 1 ]; then
+ # In some versions, no match means exit 1. In others, exit 0.
+ # FreeBSD ps.c: main() calls exit(1) if nkept == 0.
+ fail "ps -p invalid_pid should exit 1 (got ${EXIT_CODE})"
+fi
+pass "Negative test (invalid pid)"
+
+# 8. Negative test: invalid flag
+${PS_BIN} -Y > /dev/null 2>&1
+if [ $? -eq 0 ]; then
+ fail "ps -Y should fail"
+fi
+pass "Negative test (invalid flag)"
+
+# 9. Width test: -w
+${HELPER_BIN} test-ps-width-very-long-name-to-check-truncation &
+HELPER_PID=$!
+sleep 0.1
+# Record output without -w (should truncate or at least be limited)
+${PS_BIN} -u -p ${HELPER_PID} > /tmp/ps_w1
+# Record output with -ww (no limit)
+${PS_BIN} -u -ww -p ${HELPER_PID} > /tmp/ps_w2
+# Compare -w2 should be longer or equal to -w1 in terms of line length
+LEN1=$(awk '{ print length }' /tmp/ps_w1 | sort -nr | head -1)
+LEN2=$(awk '{ print length }' /tmp/ps_w2 | sort -nr | head -1)
+if [ "${LEN2}" -lt "${LEN1}" ]; then
+ fail "ps -ww output shorter than normal output"
+fi
+kill ${HELPER_PID}
+pass "Width control (-w, -ww)"
+
+# 10. Headers control: -h
+# Start 30 helpers safely.
+I=1
+while [ ${I} -le 30 ]; do
+ ${HELPER_BIN} test-ps-h-${I} &
+ H_PIDS="${H_PIDS} $!"
+ I=$((I+1))
+done
+sleep 0.1
+# Run ps -h -A (repeat headers every 22 lines or so)
+${PS_BIN} -h -A | grep "PID" | wc -l > /tmp/ps_h_count
+COUNT=$(cat /tmp/ps_h_count)
+if [ "${COUNT}" -lt 2 ]; then
+ fail "ps -h should repeat headers (got ${COUNT} headers for 30+ processes)"
+fi
+kill ${H_PIDS}
+pass "Header repetition (-h)"
+
+# Clean up
+rm -f /tmp/ps_*
+printf "${GREEN}All tests passed!${NC}\n"
+exit 0