Viewing: sh.c
📄 sh.c (Read Only) ⬅ To go back
/* AdrOS POSIX-like shell (/bin/sh)
 *
 * Features:
 *   - Variable assignment (VAR=value) and expansion ($VAR)
 *   - Environment variables (export VAR=value)
 *   - Line editing (left/right arrow keys)
 *   - Command history (up/down arrow keys)
 *   - Pipes (cmd1 | cmd2 | cmd3)
 *   - Redirections (< > >>)
 *   - Operators: ; && || &
 *   - Job control: CTRL+C (SIGINT), CTRL+Z (SIGTSTP), background (&)
 *   - Builtins: cd, exit, echo, export, unset, set, pwd, type
 *   - PATH-based command resolution
 *   - Quote handling (single and double quotes)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <termios.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/wait.h>

static struct termios orig_termios;

static void tty_raw_mode(void) {
    tcgetattr(STDIN_FILENO, &orig_termios);
    struct termios raw = orig_termios;
    raw.c_lflag &= ~(ICANON | ECHO | ISIG);
    raw.c_cc[VMIN] = 1;
    raw.c_cc[VTIME] = 0;
    tcsetattr(STDIN_FILENO, TCSANOW, &raw);
}

static void tty_restore(void) {
    tcsetattr(STDIN_FILENO, TCSANOW, &orig_termios);
}

#define LINE_MAX   512
#define MAX_ARGS   64
#define MAX_VARS   64
#define HIST_SIZE  32

/* ---- Shell variables ---- */

static struct {
    char name[64];
    char value[256];
    int  exported;
} vars[MAX_VARS];
static int nvar = 0;

static int last_status = 0;

static const char* var_get(const char* name) {
    for (int i = 0; i < nvar; i++)
        if (strcmp(vars[i].name, name) == 0) return vars[i].value;
    return getenv(name);
}

static void var_set(const char* name, const char* value, int exported) {
    for (int i = 0; i < nvar; i++) {
        if (strcmp(vars[i].name, name) == 0) {
            strncpy(vars[i].value, value, 255);
            vars[i].value[255] = '\0';
            if (exported) vars[i].exported = 1;
            return;
        }
    }
    if (nvar < MAX_VARS) {
        strncpy(vars[nvar].name, name, 63);
        vars[nvar].name[63] = '\0';
        strncpy(vars[nvar].value, value, 255);
        vars[nvar].value[255] = '\0';
        vars[nvar].exported = exported;
        nvar++;
    }
}

static void var_unset(const char* name) {
    for (int i = 0; i < nvar; i++) {
        if (strcmp(vars[i].name, name) == 0) {
            vars[i] = vars[--nvar];
            return;
        }
    }
}

/* Build envp array from exported variables */
static char env_buf[MAX_VARS][320];
static char* envp_arr[MAX_VARS + 1];

static char** build_envp(void) {
    int n = 0;
    for (int i = 0; i < nvar && n < MAX_VARS; i++) {
        if (!vars[i].exported) continue;
        snprintf(env_buf[n], sizeof(env_buf[n]), "%s=%s",
                 vars[i].name, vars[i].value);
        envp_arr[n] = env_buf[n];
        n++;
    }
    envp_arr[n] = NULL;
    return envp_arr;
}

/* ---- Command history ---- */

static char history[HIST_SIZE][LINE_MAX];
static int hist_count = 0;
static int hist_pos = 0;

static void hist_add(const char* line) {
    if (line[0] == '\0') return;
    if (hist_count > 0 && strcmp(history[(hist_count - 1) % HIST_SIZE], line) == 0)
        return;
    strncpy(history[hist_count % HIST_SIZE], line, LINE_MAX - 1);
    history[hist_count % HIST_SIZE][LINE_MAX - 1] = '\0';
    hist_count++;
}

/* ---- Line editing ---- */

static char line[LINE_MAX];

static void term_write(const char* s, int n) {
    write(STDOUT_FILENO, s, (size_t)n);
}

/* ---- Tab completion ---- */

static int tab_complete(char* buf, int* p_pos, int* p_len) {
    int pos = *p_pos;
    int len = *p_len;

    /* Find the start of the current word */
    int wstart = pos;
    while (wstart > 0 && buf[wstart - 1] != ' ' && buf[wstart - 1] != '\t')
        wstart--;

    char prefix[128];
    int plen = pos - wstart;
    if (plen <= 0 || plen >= (int)sizeof(prefix)) return 0;
    memcpy(prefix, buf + wstart, (size_t)plen);
    prefix[plen] = '\0';

    /* Determine if this is a command (first word) or filename */
    int is_cmd = 1;
    for (int i = 0; i < wstart; i++) {
        if (buf[i] != ' ' && buf[i] != '\t') { is_cmd = 0; break; }
    }

    char match[128];
    match[0] = '\0';
    int nmatches = 0;

    /* Split prefix into directory part and name part for file completion */
    char dirpath[128] = ".";
    const char* namepfx = prefix;
    char* lastsep = NULL;
    for (char* p = prefix; *p; p++) {
        if (*p == '/') lastsep = p;
    }
    if (lastsep) {
        int dlen = (int)(lastsep - prefix);
        if (dlen == 0) { dirpath[0] = '/'; dirpath[1] = '\0'; }
        else { memcpy(dirpath, prefix, (size_t)dlen); dirpath[dlen] = '\0'; }
        namepfx = lastsep + 1;
    }
    int nplen = (int)strlen(namepfx);

    if (!is_cmd || lastsep) {
        /* File/directory completion */
        int fd = open(dirpath, 0);
        if (fd >= 0) {
            char dbuf[512];
            int rc;
            while ((rc = getdents(fd, dbuf, sizeof(dbuf))) > 0) {
                int off = 0;
                while (off < rc) {
                    struct dirent* d = (struct dirent*)(dbuf + off);
                    if (d->d_reclen == 0) break;
                    if (d->d_name[0] != '.' || nplen > 0) {
                        int nlen = (int)strlen(d->d_name);
                        if (nlen >= nplen && memcmp(d->d_name, namepfx, (size_t)nplen) == 0) {
                            if (nmatches == 0) strcpy(match, d->d_name);
                            nmatches++;
                        }
                    }
                    off += d->d_reclen;
                }
            }
            close(fd);
        }
    }

    if (is_cmd && !lastsep) {
        /* Command completion: search PATH directories + builtins */
        static const char* builtins[] = {
            "cd", "exit", "echo", "export", "unset", "set", "pwd", "type", NULL
        };
        for (int i = 0; builtins[i]; i++) {
            int blen = (int)strlen(builtins[i]);
            if (blen >= plen && memcmp(builtins[i], prefix, (size_t)plen) == 0) {
                if (nmatches == 0) strcpy(match, builtins[i]);
                nmatches++;
            }
        }
        const char* path_env = var_get("PATH");
        if (!path_env) path_env = "/bin:/sbin:/usr/bin";
        char pathcopy[512];
        strncpy(pathcopy, path_env, sizeof(pathcopy) - 1);
        pathcopy[sizeof(pathcopy) - 1] = '\0';
        char* save = pathcopy;
        char* dir;
        while ((dir = save) != NULL) {
            char* sep = strchr(save, ':');
            if (sep) { *sep = '\0'; save = sep + 1; } else save = NULL;
            int fd = open(dir, 0);
            if (fd < 0) continue;
            char dbuf[512];
            int rc;
            while ((rc = getdents(fd, dbuf, sizeof(dbuf))) > 0) {
                int off = 0;
                while (off < rc) {
                    struct dirent* d = (struct dirent*)(dbuf + off);
                    if (d->d_reclen == 0) break;
                    int nlen = (int)strlen(d->d_name);
                    if (nlen >= plen && memcmp(d->d_name, prefix, (size_t)plen) == 0) {
                        if (nmatches == 0) strcpy(match, d->d_name);
                        nmatches++;
                    }
                    off += d->d_reclen;
                }
            }
            close(fd);
        }
    }

    if (nmatches != 1) return 0;

    /* Insert the completion suffix */
    int mlen = (int)strlen(match);
    int suffix_len = is_cmd && !lastsep ? mlen - plen : mlen - nplen;
    const char* suffix = is_cmd && !lastsep ? match + plen : match + nplen;
    if (suffix_len <= 0 || len + suffix_len >= LINE_MAX - 1) return 0;

    memmove(buf + pos + suffix_len, buf + pos, (size_t)(len - pos));
    memcpy(buf + pos, suffix, (size_t)suffix_len);
    len += suffix_len;
    buf[len] = '\0';
    term_write(buf + pos, len - pos);
    pos += suffix_len;
    for (int i = 0; i < len - pos; i++) term_write("\b", 1);
    *p_pos = pos;
    *p_len = len;
    return 1;
}

static int read_line_edit(void) {
    int pos = 0;
    int len = 0;
    hist_pos = hist_count;

    memset(line, 0, LINE_MAX);

    while (len < LINE_MAX - 1) {
        char c;
        int r = read(STDIN_FILENO, &c, 1);
        if (r <= 0) {
            if (len == 0) return -1;
            break;
        }

        if (c == '\n' || c == '\r') {
            term_write("\n", 1);
            break;
        }

        /* Backspace / DEL */
        if (c == '\b' || c == 127) {
            if (pos > 0) {
                memmove(line + pos - 1, line + pos, (size_t)(len - pos));
                pos--; len--;
                line[len] = '\0';
                /* Redraw: move cursor back, print rest, clear tail */
                term_write("\b", 1);
                term_write(line + pos, len - pos);
                term_write(" \b", 2);
                for (int i = 0; i < len - pos; i++) term_write("\b", 1);
            }
            continue;
        }

        /* Tab = autocomplete */
        if (c == '\t') {
            tab_complete(line, &pos, &len);
            continue;
        }

        /* Ctrl+D = EOF */
        if (c == 4) {
            if (len == 0) return -1;
            continue;
        }

        /* Ctrl+C = cancel line */
        if (c == 3) {
            term_write("^C\n", 3);
            line[0] = '\0';
            return 0;
        }

        /* Ctrl+Z = ignored at prompt (no foreground job to suspend) */
        if (c == 26) {
            continue;
        }

        /* Ctrl+A = beginning of line */
        if (c == 1) {
            while (pos > 0) { term_write("\b", 1); pos--; }
            continue;
        }

        /* Ctrl+E = end of line */
        if (c == 5) {
            term_write(line + pos, len - pos);
            pos = len;
            continue;
        }

        /* Ctrl+U = clear line */
        if (c == 21) {
            while (pos > 0) { term_write("\b", 1); pos--; }
            for (int i = 0; i < len; i++) term_write(" ", 1);
            for (int i = 0; i < len; i++) term_write("\b", 1);
            len = 0; pos = 0;
            line[0] = '\0';
            continue;
        }

        /* Escape sequences (arrow keys) */
        if (c == 27) {
            char seq[2];
            if (read(STDIN_FILENO, &seq[0], 1) <= 0) continue;
            if (seq[0] != '[') continue;
            if (read(STDIN_FILENO, &seq[1], 1) <= 0) continue;

            if (seq[1] >= '0' && seq[1] <= '9') {
                /* Extended sequence like \x1b[3~ (DELETE), \x1b[1~ (Home), \x1b[4~ (End) */
                char trail;
                if (read(STDIN_FILENO, &trail, 1) <= 0) continue;
                if (trail == '~') {
                    if (seq[1] == '3') {
                        /* DELETE key — delete char at cursor */
                        if (pos < len) {
                            memmove(line + pos, line + pos + 1, (size_t)(len - pos - 1));
                            len--;
                            line[len] = '\0';
                            term_write(line + pos, len - pos);
                            term_write(" \b", 2);
                            for (int i = 0; i < len - pos; i++) term_write("\b", 1);
                        }
                    } else if (seq[1] == '1') {
                        /* Home */
                        while (pos > 0) { term_write("\b", 1); pos--; }
                    } else if (seq[1] == '4') {
                        /* End */
                        term_write(line + pos, len - pos);
                        pos = len;
                    }
                }
                continue;
            }

            switch (seq[1]) {
            case 'A':  /* Up arrow — previous history */
                if (hist_pos > 0 && hist_pos > hist_count - HIST_SIZE) {
                    hist_pos--;
                    /* Clear current line */
                    while (pos > 0) { term_write("\b", 1); pos--; }
                    for (int i = 0; i < len; i++) term_write(" ", 1);
                    for (int i = 0; i < len; i++) term_write("\b", 1);
                    /* Load history entry */
                    strcpy(line, history[hist_pos % HIST_SIZE]);
                    len = (int)strlen(line);
                    pos = len;
                    term_write(line, len);
                }
                break;
            case 'B':  /* Down arrow — next history */
                if (hist_pos < hist_count) {
                    hist_pos++;
                    while (pos > 0) { term_write("\b", 1); pos--; }
                    for (int i = 0; i < len; i++) term_write(" ", 1);
                    for (int i = 0; i < len; i++) term_write("\b", 1);
                    if (hist_pos < hist_count) {
                        strcpy(line, history[hist_pos % HIST_SIZE]);
                    } else {
                        line[0] = '\0';
                    }
                    len = (int)strlen(line);
                    pos = len;
                    term_write(line, len);
                }
                break;
            case 'C':  /* Right arrow */
                if (pos < len) { term_write(line + pos, 1); pos++; }
                break;
            case 'D':  /* Left arrow */
                if (pos > 0) { term_write("\b", 1); pos--; }
                break;
            case 'H':  /* Home */
                while (pos > 0) { term_write("\b", 1); pos--; }
                break;
            case 'F':  /* End */
                term_write(line + pos, len - pos);
                pos = len;
                break;
            }
            continue;
        }

        /* Normal printable character */
        if (c >= ' ' && c <= '~') {
            memmove(line + pos + 1, line + pos, (size_t)(len - pos));
            line[pos] = c;
            len++; line[len] = '\0';
            term_write(line + pos, len - pos);
            pos++;
            for (int i = 0; i < len - pos; i++) term_write("\b", 1);
        }
    }

    line[len] = '\0';
    return len;
}

/* ---- Variable expansion ---- */

static void expand_vars(const char* src, char* dst, int maxlen) {
    int di = 0;
    while (*src && di < maxlen - 1) {
        if (*src == '$') {
            src++;
            if (*src == '?') {
                di += snprintf(dst + di, (size_t)(maxlen - di), "%d", last_status);
                src++;
            } else if (*src == '{') {
                src++;
                char name[64];
                int ni = 0;
                while (*src && *src != '}' && ni < 63) name[ni++] = *src++;
                name[ni] = '\0';
                if (*src == '}') src++;
                const char* val = var_get(name);
                if (val) {
                    int vl = (int)strlen(val);
                    if (di + vl < maxlen) { memcpy(dst + di, val, (size_t)vl); di += vl; }
                }
            } else {
                char name[64];
                int ni = 0;
                while ((*src >= 'A' && *src <= 'Z') || (*src >= 'a' && *src <= 'z') ||
                       (*src >= '0' && *src <= '9') || *src == '_') {
                    if (ni < 63) name[ni++] = *src;
                    src++;
                }
                name[ni] = '\0';
                const char* val = var_get(name);
                if (val) {
                    int vl = (int)strlen(val);
                    if (di + vl < maxlen) { memcpy(dst + di, val, (size_t)vl); di += vl; }
                }
            }
        } else {
            dst[di++] = *src++;
        }
    }
    dst[di] = '\0';
}

/* ---- Argument parsing with quote handling ---- */

static int parse_args(char* cmd, char** argv, int max) {
    int argc = 0;
    char* p = cmd;
    while (*p && argc < max - 1) {
        while (*p == ' ' || *p == '\t') p++;
        if (*p == '\0') break;

        char* out = p;
        argv[argc++] = out;

        while (*p && *p != ' ' && *p != '\t') {
            if (*p == '\'' ) {
                p++;
                while (*p && *p != '\'') *out++ = *p++;
                if (*p == '\'') p++;
            } else if (*p == '"') {
                p++;
                while (*p && *p != '"') *out++ = *p++;
                if (*p == '"') p++;
            } else {
                *out++ = *p++;
            }
        }
        if (*p) p++;
        *out = '\0';
    }
    argv[argc] = NULL;
    return argc;
}

/* ---- PATH resolution ---- */

static char pathbuf[256];

static const char* resolve(const char* cmd) {
    if (cmd[0] == '/' || cmd[0] == '.') return cmd;

    const char* path_env = var_get("PATH");
    if (!path_env) path_env = "/bin:/sbin:/usr/bin";

    char pathcopy[512];
    strncpy(pathcopy, path_env, sizeof(pathcopy) - 1);
    pathcopy[sizeof(pathcopy) - 1] = '\0';

    char* save = pathcopy;
    char* dir;
    while ((dir = save) != NULL) {
        char* sep = strchr(save, ':');
        if (sep) { *sep = '\0'; save = sep + 1; }
        else save = NULL;

        snprintf(pathbuf, sizeof(pathbuf), "%s/%s", dir, cmd);
        if (access(pathbuf, 0) == 0) return pathbuf;
    }
    return cmd;
}

/* ---- Helper: set foreground process group on controlling TTY ---- */

static void set_fg_pgrp(int pgrp) {
    ioctl(STDIN_FILENO, TIOCSPGRP, &pgrp);
}

/* ---- Run a single simple command ---- */

static int bg_flag = 0;  /* set by caller when trailing & detected */

static void run_simple(char* cmd) {
    /* Expand variables */
    char expanded[LINE_MAX];
    expand_vars(cmd, expanded, LINE_MAX);

    char* argv[MAX_ARGS];
    int argc = parse_args(expanded, argv, MAX_ARGS);
    if (argc == 0) return;

    /* Check for variable assignment (no command, just VAR=value) */
    if (argc == 1 && strchr(argv[0], '=') != NULL) {
        char* eq = strchr(argv[0], '=');
        *eq = '\0';
        var_set(argv[0], eq + 1, 0);
        last_status = 0;
        return;
    }

    /* Extract redirections */
    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 {
            argv[nargc++] = argv[i];
        }
    }
    argv[nargc] = NULL;
    argc = nargc;
    if (argc == 0) return;

    /* ---- Apply redirections for builtins too ---- */
    int saved_stdin = -1, saved_stdout = -1;
    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); }
    }
    if (redir_out) {
        int flags = O_WRONLY | O_CREAT;
        flags |= append ? O_APPEND : O_TRUNC;
        int fd = open(redir_out, flags);
        if (fd >= 0) { saved_stdout = dup(1); dup2(fd, 1); close(fd); }
    }

    /* ---- Builtins ---- */

    if (strcmp(argv[0], "exit") == 0) {
        int code = argc > 1 ? atoi(argv[1]) : last_status;
        exit(code);
    }

    if (strcmp(argv[0], "cd") == 0) {
        const char* dir = argc > 1 ? argv[1] : var_get("HOME");
        if (!dir) dir = "/";
        if (chdir(dir) < 0)
            fprintf(stderr, "cd: %s: No such file or directory\n", dir);
        else {
            char cwd[256];
            if (getcwd(cwd, sizeof(cwd)) >= 0)
                var_set("PWD", cwd, 1);
        }
        goto restore_redir;
    }

    if (strcmp(argv[0], "pwd") == 0) {
        char cwd[256];
        if (getcwd(cwd, sizeof(cwd)) >= 0)
            printf("%s\n", cwd);
        else
            fprintf(stderr, "pwd: error\n");
        goto restore_redir;
    }

    if (strcmp(argv[0], "export") == 0) {
        for (int i = 1; i < argc; i++) {
            char* eq = strchr(argv[i], '=');
            if (eq) {
                *eq = '\0';
                var_set(argv[i], eq + 1, 1);
            } else {
                /* Export existing variable */
                for (int j = 0; j < nvar; j++)
                    if (strcmp(vars[j].name, argv[i]) == 0)
                        vars[j].exported = 1;
            }
        }
        goto restore_redir;
    }

    if (strcmp(argv[0], "unset") == 0) {
        for (int i = 1; i < argc; i++) var_unset(argv[i]);
        goto restore_redir;
    }

    if (strcmp(argv[0], "set") == 0) {
        for (int i = 0; i < nvar; i++)
            printf("%s=%s\n", vars[i].name, vars[i].value);
        goto restore_redir;
    }

    if (strcmp(argv[0], "echo") == 0) {
        int nflag = 0;
        int start = 1;
        if (argc > 1 && strcmp(argv[1], "-n") == 0) { nflag = 1; start = 2; }
        for (int i = start; i < argc; i++) {
            if (i > start) write(STDOUT_FILENO, " ", 1);
            write(STDOUT_FILENO, argv[i], strlen(argv[i]));
        }
        if (!nflag) write(STDOUT_FILENO, "\n", 1);
        goto restore_redir;
    }

    if (strcmp(argv[0], "type") == 0) {
        for (int i = 1; i < argc; i++) {
            if (strcmp(argv[i], "cd") == 0 || strcmp(argv[i], "exit") == 0 ||
                strcmp(argv[i], "echo") == 0 || strcmp(argv[i], "export") == 0 ||
                strcmp(argv[i], "unset") == 0 || strcmp(argv[i], "set") == 0 ||
                strcmp(argv[i], "pwd") == 0 || strcmp(argv[i], "type") == 0) {
                printf("%s is a shell builtin\n", argv[i]);
            } else {
                const char* path = resolve(argv[i]);
                if (strcmp(path, argv[i]) != 0)
                    printf("%s is %s\n", argv[i], path);
                else
                    printf("%s: not found\n", argv[i]);
            }
        }
        goto restore_redir;
    }

    /* ---- External command — restore parent redirections before fork ---- */
    const char* path = resolve(argv[0]);
    char** envp = build_envp();

    int pid = fork();
    if (pid < 0) { fprintf(stderr, "sh: fork failed\n"); return; }

    if (pid == 0) {
        /* child: own process group, restore default signals */
        setpgid(0, 0);
        struct sigaction sa;
        memset(&sa, 0, sizeof(sa));
        sa.sa_handler = (uintptr_t)SIG_DFL;
        sigaction(SIGINT, &sa, NULL);
        sigaction(SIGTSTP, &sa, NULL);
        sigaction(SIGQUIT, &sa, NULL);

        if (redir_in) {
            int fd = open(redir_in, O_RDONLY);
            if (fd >= 0) { dup2(fd, 0); close(fd); }
        }
        if (redir_out) {
            int flags = O_WRONLY | O_CREAT;
            flags |= append ? O_APPEND : O_TRUNC;
            int fd = open(redir_out, flags);
            if (fd >= 0) { dup2(fd, 1); close(fd); }
        }
        execve(path, argv, envp);
        fprintf(stderr, "sh: %s: not found\n", argv[0]);
        _exit(127);
    }

    /* parent */
    setpgid(pid, pid);

    if (bg_flag) {
        /* Background: don't wait, print job info */
        printf("[bg] %d\n", pid);
        last_status = 0;
    } else {
        /* Foreground: make child the fg process group, wait, then restore */
        set_fg_pgrp(pid);
        int st;
        waitpid(pid, &st, 0);
        set_fg_pgrp(getpgrp());
        last_status = st;
    }
    goto restore_redir;

restore_redir:
    if (saved_stdout >= 0) { dup2(saved_stdout, 1); close(saved_stdout); }
    if (saved_stdin >= 0)  { dup2(saved_stdin, 0);  close(saved_stdin);  }
}

/* ---- Pipeline support ---- */

static void run_pipeline(char* cmdline) {
    /* Split on '|' (outside quotes) */
    char* cmds[8];
    int ncmds = 0;
    cmds[0] = cmdline;
    int in_sq = 0, in_dq = 0;
    for (char* p = cmdline; *p; p++) {
        if (*p == '\'' && !in_dq) in_sq = !in_sq;
        else if (*p == '"' && !in_sq) in_dq = !in_dq;
        else if (*p == '|' && !in_sq && !in_dq && ncmds < 7) {
            if (*(p + 1) == '|') { p++; continue; }  /* skip || */
            *p = '\0';
            cmds[++ncmds] = p + 1;
        }
    }
    ncmds++;

    if (ncmds == 1) {
        run_simple(cmds[0]);
        return;
    }

    /* Multi-stage pipeline */
    int prev_rd = -1;
    int pids[8];
    int pgid = 0;  /* pipeline process group = first child's PID */

    for (int i = 0; i < ncmds; i++) {
        int pfd[2] = {-1, -1};
        if (i < ncmds - 1) {
            if (pipe(pfd) < 0) {
                fprintf(stderr, "sh: pipe failed\n");
                return;
            }
        }

        pids[i] = fork();
        if (pids[i] < 0) { fprintf(stderr, "sh: fork failed\n"); return; }

        if (pids[i] == 0) {
            /* child: join pipeline process group, restore signals */
            int mypgid = pgid ? pgid : getpid();
            setpgid(0, mypgid);
            struct sigaction sa;
            memset(&sa, 0, sizeof(sa));
            sa.sa_handler = (uintptr_t)SIG_DFL;
            sigaction(SIGINT, &sa, NULL);
            sigaction(SIGTSTP, &sa, NULL);
            sigaction(SIGQUIT, &sa, NULL);

            if (prev_rd >= 0) { dup2(prev_rd, 0); close(prev_rd); }
            if (pfd[1] >= 0)  { dup2(pfd[1], 1); close(pfd[1]); }
            if (pfd[0] >= 0)  close(pfd[0]);

            /* Expand and parse this pipeline stage */
            char expanded[LINE_MAX];
            expand_vars(cmds[i], expanded, LINE_MAX);
            char* argv[MAX_ARGS];
            int argc = parse_args(expanded, argv, MAX_ARGS);
            if (argc == 0) _exit(0);
            const char* path = resolve(argv[0]);
            char** envp = build_envp();
            execve(path, argv, envp);
            fprintf(stderr, "sh: %s: not found\n", argv[0]);
            _exit(127);
        }

        /* parent: set pipeline pgid */
        if (i == 0) pgid = pids[0];
        setpgid(pids[i], pgid);

        if (prev_rd >= 0) close(prev_rd);
        if (pfd[1] >= 0)  close(pfd[1]);
        prev_rd = pfd[0];
    }

    if (prev_rd >= 0) close(prev_rd);

    if (!bg_flag) {
        /* Foreground pipeline: make it fg, wait, restore */
        set_fg_pgrp(pgid);
        for (int i = 0; i < ncmds; i++) {
            int st;
            waitpid(pids[i], &st, 0);
            if (i == ncmds - 1) last_status = st;
        }
        set_fg_pgrp(getpgrp());
    } else {
        printf("[bg] %d\n", pgid);
        last_status = 0;
    }
}

/* ---- Process a command line (handle ;, &&, ||, &) ---- */

enum { OP_NONE = 0, OP_SEMI, OP_AND, OP_OR, OP_BG };

static void process_line(char* input) {
    char* p = input;

    while (*p) {
        while (*p == ' ' || *p == '\t') p++;
        if (*p == '\0') break;

        /* Find the next operator outside quotes */
        char* start = p;
        int in_sq = 0, in_dq = 0;
        int op = OP_NONE;

        while (*p) {
            if (*p == '\'' && !in_dq) { in_sq = !in_sq; p++; continue; }
            if (*p == '"' && !in_sq)  { in_dq = !in_dq; p++; continue; }
            if (in_sq || in_dq) { p++; continue; }

            if (*p == '&' && *(p + 1) == '&') {
                *p = '\0'; p += 2; op = OP_AND; break;
            }
            if (*p == '|' && *(p + 1) == '|') {
                *p = '\0'; p += 2; op = OP_OR; break;
            }
            if (*p == ';') {
                *p = '\0'; p++; op = OP_SEMI; break;
            }
            if (*p == '&') {
                *p = '\0'; p++; op = OP_BG; break;
            }
            p++;
        }

        /* Trim leading whitespace from segment */
        while (*start == ' ' || *start == '\t') start++;
        if (*start != '\0') {
            if (op == OP_BG) {
                bg_flag = 1;
                run_pipeline(start);
                bg_flag = 0;
            } else {
                bg_flag = 0;
                run_pipeline(start);
            }
        }

        /* For &&: skip remaining commands if last failed */
        if (op == OP_AND && last_status != 0) {
            /* Skip until we hit || or ; or & or end */
            while (*p) {
                while (*p == ' ' || *p == '\t') p++;
                if (*p == '\0') break;
                int skip_sq = 0, skip_dq = 0;
                while (*p) {
                    if (*p == '\'' && !skip_dq) { skip_sq = !skip_sq; p++; continue; }
                    if (*p == '"' && !skip_sq)  { skip_dq = !skip_dq; p++; continue; }
                    if (skip_sq || skip_dq) { p++; continue; }
                    if (*p == '|' && *(p + 1) == '|') break;
                    if (*p == ';') break;
                    if (*p == '&' && *(p + 1) != '&') break;
                    if (*p == '&' && *(p + 1) == '&') { p += 2; continue; }
                    p++;
                }
                break;
            }
        }

        /* For ||: skip remaining commands if last succeeded */
        if (op == OP_OR && last_status == 0) {
            while (*p) {
                while (*p == ' ' || *p == '\t') p++;
                if (*p == '\0') break;
                int skip_sq = 0, skip_dq = 0;
                while (*p) {
                    if (*p == '\'' && !skip_dq) { skip_sq = !skip_sq; p++; continue; }
                    if (*p == '"' && !skip_sq)  { skip_dq = !skip_dq; p++; continue; }
                    if (skip_sq || skip_dq) { p++; continue; }
                    if (*p == '&' && *(p + 1) == '&') break;
                    if (*p == ';') break;
                    if (*p == '&' && *(p + 1) != '&') break;
                    if (*p == '|' && *(p + 1) == '|') { p += 2; continue; }
                    p++;
                }
                break;
            }
        }
    }
}

/* ---- Prompt ---- */

static void print_prompt(void) {
    const char* user = var_get("USER");
    const char* host = var_get("HOSTNAME");
    char cwd[256];

    if (!user) user = "root";
    if (!host) host = "adros";

    if (getcwd(cwd, sizeof(cwd)) < 0) strcpy(cwd, "?");

    printf("%s@%s:%s$ ", user, host, cwd);
    fflush(stdout);
}

/* ---- Main ---- */

int main(int argc, char** argv, char** envp) {
    (void)argc;
    (void)argv;

    /* Import environment variables */
    if (envp) {
        for (int i = 0; envp[i]; i++) {
            char* eq = strchr(envp[i], '=');
            if (eq) {
                char name[64];
                int nlen = (int)(eq - envp[i]);
                if (nlen > 63) nlen = 63;
                memcpy(name, envp[i], (size_t)nlen);
                name[nlen] = '\0';
                var_set(name, eq + 1, 1);
            }
        }
    }

    /* Set defaults */
    if (!var_get("PATH"))
        var_set("PATH", "/bin:/sbin:/usr/bin", 1);
    if (!var_get("HOME"))
        var_set("HOME", "/", 1);

    /* Job control: create session + process group, become fg */
    setsid();
    set_fg_pgrp(getpgrp());

    /* Ignore job control signals in the shell itself */
    struct sigaction sa_ign;
    memset(&sa_ign, 0, sizeof(sa_ign));
    sa_ign.sa_handler = (uintptr_t)SIG_IGN;
    sigaction(SIGINT, &sa_ign, NULL);
    sigaction(SIGTSTP, &sa_ign, NULL);
    sigaction(SIGQUIT, &sa_ign, NULL);

    tty_raw_mode();

    print_prompt();
    while (1) {
        int len = read_line_edit();
        if (len < 0) break;
        if (len > 0) {
            hist_add(line);
            tty_restore();
            process_line(line);
            tty_raw_mode();
        }
        print_prompt();
    }

    tty_restore();
    return last_status;
}