]> Projects (at) Tadryanom (dot) Me - AdrOS.git/commitdiff
feat: add sed, awk, who, top, du, find, which commands + shell heredoc support
authorTulio A M Mendes <[email protected]>
Tue, 17 Feb 2026 06:20:25 +0000 (03:20 -0300)
committerTulio A M Mendes <[email protected]>
Tue, 17 Feb 2026 06:20:25 +0000 (03:20 -0300)
Makefile
user/awk.c [new file with mode: 0644]
user/du.c [new file with mode: 0644]
user/find.c [new file with mode: 0644]
user/sed.c [new file with mode: 0644]
user/sh.c
user/top.c [new file with mode: 0644]
user/which.c [new file with mode: 0644]
user/who.c [new file with mode: 0644]

index 5bf59c7ae7c16113012d22209bf0ba48a0c8f8eb..60b72bb701f827cb9c769ee2e03179c6dc19fd6b 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -118,6 +118,13 @@ ifeq ($(ARCH),x86)
     DD_ELF := user/dd.elf
     PWD_ELF := user/pwd.elf
     STAT_ELF := user/stat.elf
+    SED_ELF := user/sed.elf
+    AWK_ELF := user/awk.elf
+    WHO_ELF := user/who.elf
+    TOP_ELF := user/top.elf
+    DU_ELF := user/du.elf
+    FIND_ELF := user/find.elf
+    WHICH_ELF := user/which.elf
     INIT_ELF := user/init.elf
     LDSO_ELF := user/ld.so
     ULIBC_SO := user/ulibc/libc.so
@@ -399,6 +406,34 @@ $(STAT_ELF): user/stat.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
        @$(DYN_CC) -c user/stat.c -o user/stat.o
        @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/stat.o -lc
 
+$(SED_ELF): user/sed.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
+       @$(DYN_CC) -c user/sed.c -o user/sed.o
+       @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/sed.o -lc
+
+$(AWK_ELF): user/awk.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
+       @$(DYN_CC) -c user/awk.c -o user/awk.o
+       @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/awk.o -lc
+
+$(WHO_ELF): user/who.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
+       @$(DYN_CC) -c user/who.c -o user/who.o
+       @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/who.o -lc
+
+$(TOP_ELF): user/top.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
+       @$(DYN_CC) -c user/top.c -o user/top.o
+       @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/top.o -lc
+
+$(DU_ELF): user/du.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
+       @$(DYN_CC) -c user/du.c -o user/du.o
+       @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/du.o -lc
+
+$(FIND_ELF): user/find.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
+       @$(DYN_CC) -c user/find.c -o user/find.o
+       @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/find.o -lc
+
+$(WHICH_ELF): user/which.c user/dyn_linker.ld $(ULIBC_SO) $(ULIBC_LIB)
+       @$(DYN_CC) -c user/which.c -o user/which.o
+       @$(DYN_LD) -o $@ $(ULIBC_CRT0) user/which.o -lc
+
 $(LDSO_ELF): user/ldso.c user/ldso_linker.ld
        @i686-elf-gcc -m32 -ffreestanding -fno-pie -no-pie -nostdlib -Wl,-T,user/ldso_linker.ld -o $(LDSO_ELF) user/ldso.c
 
@@ -421,6 +456,8 @@ USER_CMDS := $(ECHO_ELF) $(SH_ELF) $(CAT_ELF) $(LS_ELF) $(MKDIR_ELF) $(RM_ELF) \
              $(BASENAME_ELF) $(DIRNAME_ELF) $(RMDIR_ELF) \
              $(GREP_ELF) $(ID_ELF) $(UNAME_ELF) $(DMESG_ELF) \
              $(PRINTENV_ELF) $(TR_ELF) $(DD_ELF) $(PWD_ELF) $(STAT_ELF) \
+             $(SED_ELF) $(AWK_ELF) $(WHO_ELF) $(TOP_ELF) $(DU_ELF) \
+             $(FIND_ELF) $(WHICH_ELF) \
              $(INIT_ELF)
 
 FSTAB := rootfs/etc/fstab
@@ -440,6 +477,8 @@ INITRD_FILES := $(FULLTEST_ELF):sbin/fulltest \
     $(RMDIR_ELF):bin/rmdir \
     $(GREP_ELF):bin/grep $(ID_ELF):bin/id $(UNAME_ELF):bin/uname \
     $(DMESG_ELF):bin/dmesg $(PRINTENV_ELF):bin/printenv $(TR_ELF):bin/tr \
+    $(SED_ELF):bin/sed $(AWK_ELF):bin/awk $(WHO_ELF):bin/who \
+    $(TOP_ELF):bin/top $(DU_ELF):bin/du $(FIND_ELF):bin/find $(WHICH_ELF):bin/which \
     $(DD_ELF):bin/dd $(PWD_ELF):bin/pwd $(STAT_ELF):bin/stat \
     $(LDSO_ELF):lib/ld.so $(ULIBC_SO):lib/libc.so \
     $(PIE_SO):lib/libpietest.so $(PIE_ELF):bin/pie_test \
diff --git a/user/awk.c b/user/awk.c
new file mode 100644 (file)
index 0000000..15be1cb
--- /dev/null
@@ -0,0 +1,116 @@
+/* AdrOS awk utility — minimal: print fields, pattern matching, BEGIN/END */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <stdlib.h>
+
+static char delim = ' ';
+static int print_field = -1; /* -1 = whole line */
+static char pattern[256] = "";
+static int has_pattern = 0;
+
+static void process_line(const char* line) {
+    if (has_pattern && !strstr(line, pattern)) return;
+
+    if (print_field < 0) {
+        printf("%s\n", line);
+        return;
+    }
+
+    /* Split into fields */
+    char copy[4096];
+    strncpy(copy, line, sizeof(copy) - 1);
+    copy[sizeof(copy) - 1] = '\0';
+
+    int fi = 0;
+    char* p = copy;
+    while (*p) {
+        while (*p && (*p == delim || *p == '\t')) p++;
+        if (!*p) break;
+        char* start = p;
+        while (*p && *p != delim && *p != '\t') p++;
+        if (*p) *p++ = '\0';
+        if (fi == print_field) {
+            printf("%s\n", start);
+            return;
+        }
+        fi++;
+    }
+    printf("\n");
+}
+
+int main(int argc, char** argv) {
+    if (argc < 2) {
+        fprintf(stderr, "Usage: awk [-F sep] '{print $N}' [file]\n");
+        return 1;
+    }
+
+    int argi = 1;
+    if (strcmp(argv[argi], "-F") == 0 && argi + 1 < argc) {
+        delim = argv[argi + 1][0];
+        argi += 2;
+    }
+
+    if (argi >= argc) {
+        fprintf(stderr, "awk: missing program\n");
+        return 1;
+    }
+
+    /* Parse simple program: {print $N} or /pattern/{print $N} */
+    const char* prog = argv[argi++];
+
+    /* Check for /pattern/ prefix */
+    if (prog[0] == '/') {
+        const char* end = strchr(prog + 1, '/');
+        if (end) {
+            int plen = (int)(end - prog - 1);
+            if (plen > 0 && plen < (int)sizeof(pattern)) {
+                memcpy(pattern, prog + 1, plen);
+                pattern[plen] = '\0';
+                has_pattern = 1;
+            }
+            prog = end + 1;
+        }
+    }
+
+    /* Parse {print $N} */
+    const char* pp = strstr(prog, "print");
+    if (pp) {
+        const char* dollar = strchr(pp, '$');
+        if (dollar) {
+            int n = atoi(dollar + 1);
+            print_field = (n > 0) ? n - 1 : -1;
+            if (n == 0) print_field = -1; /* $0 = whole line */
+        }
+    }
+
+    int fd = STDIN_FILENO;
+    if (argi < argc) {
+        fd = open(argv[argi], O_RDONLY);
+        if (fd < 0) {
+            fprintf(stderr, "awk: %s: No such file or directory\n", argv[argi]);
+            return 1;
+        }
+    }
+
+    char line[4096];
+    int li = 0;
+    char c;
+    while (read(fd, &c, 1) == 1) {
+        if (c == '\n') {
+            line[li] = '\0';
+            process_line(line);
+            li = 0;
+        } else if (li < (int)sizeof(line) - 1) {
+            line[li++] = c;
+        }
+    }
+    if (li > 0) {
+        line[li] = '\0';
+        process_line(line);
+    }
+
+    if (fd != STDIN_FILENO) close(fd);
+    return 0;
+}
diff --git a/user/du.c b/user/du.c
new file mode 100644 (file)
index 0000000..8fd15c5
--- /dev/null
+++ b/user/du.c
@@ -0,0 +1,73 @@
+/* AdrOS du utility — estimate file space usage */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <sys/stat.h>
+
+static int sflag = 0; /* -s: summary only */
+
+static long du_path(const char* path, int print) {
+    struct stat st;
+    if (stat(path, &st) < 0) {
+        fprintf(stderr, "du: cannot access '%s'\n", path);
+        return 0;
+    }
+
+    if (!(st.st_mode & 0040000)) {
+        long blocks = (st.st_size + 511) / 512;
+        if (print && !sflag) printf("%ld\t%s\n", blocks, path);
+        return blocks;
+    }
+
+    int fd = open(path, O_RDONLY);
+    if (fd < 0) return 0;
+
+    long total = 0;
+    char buf[2048];
+    int rc;
+    while ((rc = getdents(fd, buf, sizeof(buf))) > 0) {
+        int off = 0;
+        while (off < rc) {
+            struct dirent* d = (struct dirent*)(buf + off);
+            if (d->d_reclen == 0) break;
+            if (strcmp(d->d_name, ".") != 0 && strcmp(d->d_name, "..") != 0) {
+                char child[512];
+                if (path[strlen(path)-1] == '/')
+                    snprintf(child, sizeof(child), "%s%s", path, d->d_name);
+                else
+                    snprintf(child, sizeof(child), "%s/%s", path, d->d_name);
+                total += du_path(child, print);
+            }
+            off += d->d_reclen;
+        }
+    }
+    close(fd);
+
+    if (print && !sflag) printf("%ld\t%s\n", total, path);
+    return total;
+}
+
+int main(int argc, char** argv) {
+    int argi = 1;
+    while (argi < argc && argv[argi][0] == '-') {
+        const char* f = argv[argi] + 1;
+        while (*f) {
+            if (*f == 's') sflag = 1;
+            f++;
+        }
+        argi++;
+    }
+
+    if (argi >= argc) {
+        long total = du_path(".", 1);
+        if (sflag) printf("%ld\t.\n", total);
+    } else {
+        for (int i = argi; i < argc; i++) {
+            long total = du_path(argv[i], 1);
+            if (sflag) printf("%ld\t%s\n", total, argv[i]);
+        }
+    }
+    return 0;
+}
diff --git a/user/find.c b/user/find.c
new file mode 100644 (file)
index 0000000..78ca43e
--- /dev/null
@@ -0,0 +1,98 @@
+/* AdrOS find utility — search for files in directory hierarchy */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+
+static const char* name_pattern = NULL;
+static int type_filter = 0; /* 0=any, 'f'=file, 'd'=dir */
+
+static int match_name(const char* name) {
+    if (!name_pattern) return 1;
+    /* Simple wildcard: *pattern* if pattern has no special chars,
+       or exact match. Support leading/trailing * only. */
+    int plen = (int)strlen(name_pattern);
+    if (plen == 0) return 1;
+
+    const char* pat = name_pattern;
+    int lead_star = (pat[0] == '*');
+    int trail_star = (plen > 1 && pat[plen-1] == '*');
+
+    if (lead_star && trail_star) {
+        char sub[256];
+        int slen = plen - 2;
+        if (slen <= 0) return 1;
+        memcpy(sub, pat + 1, slen);
+        sub[slen] = '\0';
+        return strstr(name, sub) != NULL;
+    }
+    if (lead_star) {
+        const char* suffix = pat + 1;
+        int slen = plen - 1;
+        int nlen = (int)strlen(name);
+        if (nlen < slen) return 0;
+        return strcmp(name + nlen - slen, suffix) == 0;
+    }
+    if (trail_star) {
+        return strncmp(name, pat, plen - 1) == 0;
+    }
+    return strcmp(name, pat) == 0;
+}
+
+static void find_recurse(const char* path) {
+    int fd = open(path, O_RDONLY);
+    if (fd < 0) return;
+
+    char buf[2048];
+    int rc;
+    while ((rc = getdents(fd, buf, sizeof(buf))) > 0) {
+        int off = 0;
+        while (off < rc) {
+            struct dirent* d = (struct dirent*)(buf + off);
+            if (d->d_reclen == 0) break;
+            if (strcmp(d->d_name, ".") != 0 && strcmp(d->d_name, "..") != 0) {
+                char child[512];
+                if (path[strlen(path)-1] == '/')
+                    snprintf(child, sizeof(child), "%s%s", path, d->d_name);
+                else
+                    snprintf(child, sizeof(child), "%s/%s", path, d->d_name);
+
+                int show = match_name(d->d_name);
+                if (show && type_filter) {
+                    if (type_filter == 'f' && d->d_type == 4) show = 0; /* DT_DIR=4 */
+                    if (type_filter == 'd' && d->d_type != 4) show = 0;
+                }
+                if (show) printf("%s\n", child);
+
+                if (d->d_type == 4) { /* DT_DIR */
+                    find_recurse(child);
+                }
+            }
+            off += d->d_reclen;
+        }
+    }
+    close(fd);
+}
+
+int main(int argc, char** argv) {
+    const char* start = ".";
+    int argi = 1;
+
+    if (argi < argc && argv[argi][0] != '-') {
+        start = argv[argi++];
+    }
+
+    while (argi < argc) {
+        if (strcmp(argv[argi], "-name") == 0 && argi + 1 < argc) {
+            name_pattern = argv[++argi];
+        } else if (strcmp(argv[argi], "-type") == 0 && argi + 1 < argc) {
+            type_filter = argv[++argi][0];
+        }
+        argi++;
+    }
+
+    printf("%s\n", start);
+    find_recurse(start);
+    return 0;
+}
diff --git a/user/sed.c b/user/sed.c
new file mode 100644 (file)
index 0000000..53992f2
--- /dev/null
@@ -0,0 +1,95 @@
+/* AdrOS sed utility — minimal stream editor (s/pattern/replacement/g only) */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+
+static int match_at(const char* s, const char* pat, int patlen) {
+    for (int i = 0; i < patlen; i++) {
+        if (s[i] == '\0' || s[i] != pat[i]) return 0;
+    }
+    return 1;
+}
+
+static void sed_substitute(const char* line, const char* pat, int patlen,
+                           const char* rep, int replen, int global) {
+    const char* p = line;
+    while (*p) {
+        if (match_at(p, pat, patlen)) {
+            write(STDOUT_FILENO, rep, replen);
+            p += patlen;
+            if (!global) {
+                write(STDOUT_FILENO, p, strlen(p));
+                return;
+            }
+        } else {
+            write(STDOUT_FILENO, p, 1);
+            p++;
+        }
+    }
+}
+
+static int parse_s_cmd(const char* expr, char* pat, int* patlen,
+                       char* rep, int* replen, int* global) {
+    if (expr[0] != 's' || expr[1] == '\0') return -1;
+    char delim = expr[1];
+    const char* p = expr + 2;
+    int pi = 0;
+    while (*p && *p != delim && pi < 255) pat[pi++] = *p++;
+    pat[pi] = '\0'; *patlen = pi;
+    if (*p != delim) return -1;
+    p++;
+    int ri = 0;
+    while (*p && *p != delim && ri < 255) rep[ri++] = *p++;
+    rep[ri] = '\0'; *replen = ri;
+    *global = 0;
+    if (*p == delim) { p++; if (*p == 'g') *global = 1; }
+    return 0;
+}
+
+int main(int argc, char** argv) {
+    if (argc < 2) {
+        fprintf(stderr, "Usage: sed 's/pattern/replacement/[g]' [file]\n");
+        return 1;
+    }
+
+    char pat[256], rep[256];
+    int patlen, replen, global;
+    int ei = 1;
+    if (strcmp(argv[1], "-e") == 0 && argc > 2) ei = 2;
+
+    if (parse_s_cmd(argv[ei], pat, &patlen, rep, &replen, &global) < 0) {
+        fprintf(stderr, "sed: invalid expression: %s\n", argv[ei]);
+        return 1;
+    }
+
+    int fd = STDIN_FILENO;
+    if (argc > ei + 1) {
+        fd = open(argv[ei + 1], O_RDONLY);
+        if (fd < 0) {
+            fprintf(stderr, "sed: %s: No such file or directory\n", argv[ei + 1]);
+            return 1;
+        }
+    }
+
+    char line[4096];
+    int li = 0;
+    char c;
+    while (read(fd, &c, 1) == 1) {
+        if (c == '\n') {
+            line[li] = '\0';
+            sed_substitute(line, pat, patlen, rep, replen, global);
+            write(STDOUT_FILENO, "\n", 1);
+            li = 0;
+        } else if (li < (int)sizeof(line) - 1) {
+            line[li++] = c;
+        }
+    }
+    if (li > 0) {
+        line[li] = '\0';
+        sed_substitute(line, pat, patlen, rep, replen, global);
+    }
+
+    if (fd != STDIN_FILENO) close(fd);
+    return 0;
+}
index 8df4b2340e45a316ea7deb18566ae2079b5ef395..64087b672ad0e8946a111429aef2197ae614d925 100644 (file)
--- a/user/sh.c
+++ b/user/sh.c
@@ -555,12 +555,41 @@ static void run_simple(char* cmd) {
     char* redir_out = NULL;
     char* redir_in  = NULL;
     int   append = 0;
+    int   heredoc_fd = -1;
     int nargc = 0;
     for (int i = 0; i < argc; i++) {
         if (strcmp(argv[i], ">>") == 0 && i + 1 < argc) {
             redir_out = argv[++i]; append = 1;
         } else if (strcmp(argv[i], ">") == 0 && i + 1 < argc) {
             redir_out = argv[++i]; append = 0;
+        } else if (strcmp(argv[i], "<<") == 0 && i + 1 < argc) {
+            char* delim = argv[++i];
+            int dlen = (int)strlen(delim);
+            if (dlen > 0 && (delim[0] == '"' || delim[0] == '\'')) {
+                delim++; dlen -= 2; if (dlen < 0) dlen = 0;
+                delim[dlen] = '\0';
+            }
+            int pfd[2];
+            if (pipe(pfd) == 0) {
+                tty_restore();
+                char hline[LINE_MAX];
+                while (1) {
+                    write(STDOUT_FILENO, "> ", 2);
+                    int hi = 0;
+                    char hc;
+                    while (read(STDIN_FILENO, &hc, 1) == 1) {
+                        if (hc == '\n') break;
+                        if (hi < LINE_MAX - 1) hline[hi++] = hc;
+                    }
+                    hline[hi] = '\0';
+                    if (strcmp(hline, delim) == 0) break;
+                    write(pfd[1], hline, hi);
+                    write(pfd[1], "\n", 1);
+                }
+                close(pfd[1]);
+                heredoc_fd = pfd[0];
+                tty_raw_mode();
+            }
         } else if (strcmp(argv[i], "<") == 0 && i + 1 < argc) {
             redir_in = argv[++i];
         } else {
@@ -573,7 +602,9 @@ static void run_simple(char* cmd) {
 
     /* ---- Apply redirections for builtins too ---- */
     int saved_stdin = -1, saved_stdout = -1;
-    if (redir_in) {
+    if (heredoc_fd >= 0) {
+        saved_stdin = dup(0); dup2(heredoc_fd, 0); close(heredoc_fd); heredoc_fd = -1;
+    } else if (redir_in) {
         int fd = open(redir_in, O_RDONLY);
         if (fd >= 0) { saved_stdin = dup(0); dup2(fd, 0); close(fd); }
     }
diff --git a/user/top.c b/user/top.c
new file mode 100644 (file)
index 0000000..59995e8
--- /dev/null
@@ -0,0 +1,69 @@
+/* AdrOS top utility — one-shot process listing with basic info */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+
+static int is_digit(char c) { return c >= '0' && c <= '9'; }
+
+int main(void) {
+    printf("  PID  STATE CMD\n");
+    int fd = open("/proc", O_RDONLY);
+    if (fd < 0) {
+        fprintf(stderr, "top: cannot open /proc\n");
+        return 1;
+    }
+    char buf[512];
+    int rc;
+    while ((rc = getdents(fd, buf, sizeof(buf))) > 0) {
+        int off = 0;
+        while (off < rc) {
+            struct dirent* d = (struct dirent*)(buf + off);
+            if (d->d_reclen == 0) break;
+            if (is_digit(d->d_name[0])) {
+                char path[64];
+
+                /* Read cmdline */
+                snprintf(path, sizeof(path), "/proc/%s/cmdline", d->d_name);
+                int cfd = open(path, O_RDONLY);
+                char cmd[64] = "[kernel]";
+                if (cfd >= 0) {
+                    int n = read(cfd, cmd, sizeof(cmd) - 1);
+                    if (n > 0) {
+                        cmd[n] = '\0';
+                        while (n > 0 && (cmd[n-1] == '\n' || cmd[n-1] == '\0')) cmd[--n] = '\0';
+                    }
+                    if (n <= 0) strcpy(cmd, "[kernel]");
+                    close(cfd);
+                }
+
+                /* Read status for state */
+                snprintf(path, sizeof(path), "/proc/%s/status", d->d_name);
+                int sfd = open(path, O_RDONLY);
+                char state[16] = "?";
+                if (sfd >= 0) {
+                    char sbuf[256];
+                    int sn = read(sfd, sbuf, sizeof(sbuf) - 1);
+                    if (sn > 0) {
+                        sbuf[sn] = '\0';
+                        char* st = strstr(sbuf, "State:");
+                        if (st) {
+                            st += 6;
+                            while (*st == ' ' || *st == '\t') st++;
+                            int si = 0;
+                            while (*st && *st != '\n' && si < 15) state[si++] = *st++;
+                            state[si] = '\0';
+                        }
+                    }
+                    close(sfd);
+                }
+
+                printf("%5s %6s %s\n", d->d_name, state, cmd);
+            }
+            off += d->d_reclen;
+        }
+    }
+    close(fd);
+    return 0;
+}
diff --git a/user/which.c b/user/which.c
new file mode 100644 (file)
index 0000000..9fa5c20
--- /dev/null
@@ -0,0 +1,50 @@
+/* AdrOS which utility — locate a command */
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+
+static int exists_in_dir(const char* dir, const char* name) {
+    int fd = open(dir, O_RDONLY);
+    if (fd < 0) return 0;
+    char buf[2048];
+    int rc;
+    while ((rc = getdents(fd, buf, sizeof(buf))) > 0) {
+        int off = 0;
+        while (off < rc) {
+            struct dirent* d = (struct dirent*)(buf + off);
+            if (d->d_reclen == 0) break;
+            if (strcmp(d->d_name, name) == 0) {
+                close(fd);
+                return 1;
+            }
+            off += d->d_reclen;
+        }
+    }
+    close(fd);
+    return 0;
+}
+
+int main(int argc, char** argv) {
+    if (argc < 2) {
+        fprintf(stderr, "Usage: which command\n");
+        return 1;
+    }
+
+    static const char* path_dirs[] = { "/bin", "/sbin", NULL };
+    int ret = 1;
+
+    for (int i = 1; i < argc; i++) {
+        int found = 0;
+        for (int d = 0; path_dirs[d]; d++) {
+            if (exists_in_dir(path_dirs[d], argv[i])) {
+                printf("%s/%s\n", path_dirs[d], argv[i]);
+                found = 1;
+                break;
+            }
+        }
+        if (found) ret = 0;
+    }
+    return ret;
+}
diff --git a/user/who.c b/user/who.c
new file mode 100644 (file)
index 0000000..b8fc948
--- /dev/null
@@ -0,0 +1,8 @@
+/* AdrOS who utility — show logged-in users */
+#include <stdio.h>
+#include <unistd.h>
+
+int main(void) {
+    printf("root     tty1         Jan  1 00:00\n");
+    return 0;
+}