summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--GNUmakefile35
-rw-r--r--README.md71
-rw-r--r--extern.h124
-rw-r--r--fmt.c48
-rw-r--r--keyword.c249
-rw-r--r--nlist.c76
-rw-r--r--print.c596
-rw-r--r--ps.11020
-rw-r--r--ps.c638
-rw-r--r--ps.h205
-rw-r--r--tests/spin_helper.c45
-rw-r--r--tests/test.sh147
13 files changed, 3257 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..3193332093
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+build/
+out/
+*.o
diff --git a/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000000..eb27646501
--- /dev/null
+++ b/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/README.md b/README.md
new file mode 100644
index 0000000000..4d69c1bc68
--- /dev/null
+++ b/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/extern.h b/extern.h
new file mode 100644
index 0000000000..b0b18240da
--- /dev/null
+++ b/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/fmt.c b/fmt.c
new file mode 100644
index 0000000000..83fb445fd2
--- /dev/null
+++ b/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/keyword.c b/keyword.c
new file mode 100644
index 0000000000..e53771201b
--- /dev/null
+++ b/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/nlist.c b/nlist.c
new file mode 100644
index 0000000000..ae16581981
--- /dev/null
+++ b/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/print.c b/print.c
new file mode 100644
index 0000000000..b9fd40d300
--- /dev/null
+++ b/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/ps.1 b/ps.1
new file mode 100644
index 0000000000..fb93a6b797
--- /dev/null
+++ b/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/ps.c b/ps.c
new file mode 100644
index 0000000000..9c910f2a19
--- /dev/null
+++ b/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/ps.h b/ps.h
new file mode 100644
index 0000000000..fd18e6ef8f
--- /dev/null
+++ b/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/tests/spin_helper.c b/tests/spin_helper.c
new file mode 100644
index 0000000000..eef1d0e3b7
--- /dev/null
+++ b/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/tests/test.sh b/tests/test.sh
new file mode 100644
index 0000000000..9bfc009a99
--- /dev/null
+++ b/tests/test.sh
@@ -0,0 +1,147 @@
+#!/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"
+}
+
+# Compile helper if not already there
+HELPER_DIR="$(dirname "$0")"
+OUT_DIR="$(dirname "${PS_BIN}")"
+HELPER_BIN="${OUT_DIR}/spin_helper"
+
+if [ ! -f "${HELPER_BIN}" ]; then
+ cc -O2 "${HELPER_DIR}/spin_helper.c" -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