From: Trent Huber Date: Fri, 23 May 2025 08:26:04 +0000 (-0400) Subject: Initial refactoring X-Git-Url: https://trenthuber.com/code?a=commitdiff_plain;h=90fdedb0fbbcb49a43310e06cffcfa3240cb7057;p=thus.git Initial refactoring --- diff --git a/build.c b/build.c index bb28a1b..ff40a75 100644 --- a/build.c +++ b/build.c @@ -1,11 +1,29 @@ #include "external/cbs/cbs.c" +#define SRCDIR "src/" +#define SRC \ + SRCDIR "builtins", \ + SRCDIR "cmd", \ + SRCDIR "history", \ + SRCDIR "input", \ + SRCDIR "main", \ + SRCDIR "pg", \ + SRCDIR "term", \ + SRCDIR "utils" + +#define BINDIR "bin/" +#define ASH BINDIR "ash" + int main(void) { + char **src; + build(NULL); - compile("main", NULL); + src = (char *[]){SRC, NULL}; + cflags = (char *[]){"-ferror-limit=1", NULL}; + while (*src) compile(*src++, NULL); - load('x', "ash", "main", NULL); + load('x', ASH, SRC, NULL); return EXIT_SUCCESS; } diff --git a/main.c b/main.c deleted file mode 100644 index 0c3b80d..0000000 --- a/main.c +++ /dev/null @@ -1,618 +0,0 @@ -#include -#include -#include -#include // TODO: Use sigaction for portability? -#include -#include -#include -#include -#include -#include -#include - -struct termios canonical, raw; - -void *allocate(size_t s) { - void *r; - - if (!(r = malloc(s))) err(EXIT_FAILURE, "Memory allocation"); - - return memset(r, 0, s); -} - -#define BUFLEN 1024 -#define HISTLEN 100 -#define PROMPT "% " -#define HISTNAME ".ashhistory" - -// TODO: This could be its own struct and use macros like the stopped process table -char history[HISTLEN][BUFLEN], (*hb)[BUFLEN], (*hc)[BUFLEN], (*ht)[BUFLEN]; -static char *histpath; - -void readhist(void) { - char *homepath; - FILE *histfile; - int e; - - if (!(homepath = getenv("HOME"))) - errx(EXIT_FAILURE, "HOME environment variable does not exist"); - histpath = allocate(strlen(homepath) + 1 + strlen(HISTNAME) + 1); - strcpy(histpath, homepath); - strcat(histpath, "/"); - strcat(histpath, HISTNAME); - - ht = hc = hb = history; - if (!(histfile = fopen(histpath, "r"))) { - if (errno == ENOENT) return; - err(EXIT_FAILURE, "Unable to open history file for reading"); - } - while (fgets(*ht, BUFLEN, histfile)) { - *(*ht + strlen(*ht) - 1) = '\0'; // Get rid of newline - ht = history + (ht - history + 1) % HISTLEN; - if (ht == hb) hb = NULL; - } - - e = ferror(histfile) || !feof(histfile); - if (fclose(histfile) == EOF) err(EXIT_FAILURE, "Unable to close history file"); - if (e) err(EXIT_FAILURE, "Unable to read from history file"); - - if (!hb) hb = history + (ht - history + 1) % HISTLEN; - hc = ht; -} - -void writehist(void) { - FILE *histfile; - - if (!(histfile = fopen(histpath, "w"))) { - warn("Unable to open history file for writing"); - return; - } - - while (hb != ht) { - if (fputs(*hb, histfile) == EOF) { - warn("Unable to write to history file"); - break; - } - if (fputc('\n', histfile) == EOF) { - warn("Unable to write newline to history file"); - break; - } - hb = history + (hb - history + 1) % HISTLEN; - } - - if (fclose(histfile) == EOF) warn("Unable to close history stream"); -} - -void ashexit(void) { - if (tcsetattr(STDIN_FILENO, TCSANOW, &canonical) == -1) - warn("Unable to return to initial terminal settings"); -} - -enum { - CTRLC = '\003', - CTRLD, - ESCAPE = '\033', - UP = 'A', - DOWN, - RIGHT, - LEFT, - BACKSPACE = '\177', -}; - -char *getinput(void) { - static char result[BUFLEN]; - char *cursor, *end; - int c, i; - - fputs(PROMPT, stdout); - *result = '\0'; - for (end = cursor = result; (c = getchar()) != '\n';) { - if (c >= ' ' && c <= '~') { - if (end - result == BUFLEN - 1) continue; - memmove(cursor + 1, cursor, end - cursor); - *cursor++ = c; - *++end = '\0'; - - putchar(c); - fputs(cursor, stdout); - for (i = end - cursor; i > 0; --i) putchar('\b'); - } else switch (c) { - case CTRLC: - puts("^C"); - return NULL; - case CTRLD: - puts("^D"); - exit(EXIT_SUCCESS); - case ESCAPE: - if ((c = getchar()) != '[') { - ungetc(c, stdin); - break; - } - switch ((c = getchar())) { - case UP: - case DOWN: - if (hc == (c == UP ? hb : ht)) continue; - if (strcmp(*hc, result) != 0) strcpy(*ht, result); - - putchar('\r'); - for (i = end - result + strlen(PROMPT); i > 0; --i) putchar(' '); - putchar('\r'); - fputs(PROMPT, stdout); - - hc = history + ((hc - history + (c == UP ? -1 : 1)) % HISTLEN + HISTLEN) - % HISTLEN; - strcpy(result, *hc); - end = cursor = result + strlen(result); - - fputs(result, stdout); - break; - case LEFT: - if (cursor > result) { - putchar('\b'); - --cursor; - } - break; - case RIGHT: - if (cursor < end) putchar(*cursor++); - break; - } - break; - case BACKSPACE: - if (cursor == result) continue; - memmove(cursor - 1, cursor, end - cursor); - --cursor; - *--end = '\0'; - - putchar('\b'); - fputs(cursor, stdout); - putchar(' '); - for (i = end - cursor + 1; i > 0; --i) putchar('\b'); - - break; - } - } - fpurge(stdout); - if (result == end) return NULL; - strcpy(*ht, result); - ht = history + (ht - history + 1) % HISTLEN; - hc = ht; - if (ht == hb) hb = history + (hb - history + 1) % HISTLEN; - return result; -} - -int delimiter(char c) { - return c == ' ' || c == '&' || c == '|' || c == ';' || c == '`' || c == '\0'; -} - -enum terminator { - BG, - AND, - PIPE, - OR, - SEMI, -}; - -enum terminator strp2term(char **strp) { - enum terminator result; - char *p; - - switch (*(p = (*strp)++)) { - case '&': - result = **strp == '&' ? (++*strp, AND) : BG; - break; - case '|': - result = **strp == '|' ? (++*strp, OR) : PIPE; - break; - default: - result = SEMI; - } - *p = '\0'; - - return result; -} - -struct cmd { - char **args; - enum terminator type; -}; - -struct cmd *getcmds(char *b) { - size_t i, j; - char **t; - struct cmd *c; - enum terminator term; - static char *tokens[BUFLEN + 1]; - static struct cmd result[BUFLEN / 2 + 1]; - - *tokens = NULL; - t = tokens + 1; - c = result; - while (*b == ' ') ++b; - c->args = t; - while (*b) switch (*b) { - default: - *t++ = b; - while (!delimiter(*b)) ++b; - break; - case ' ': - *b++ = '\0'; - while (*b == ' ') ++b; - break; - case ';': - case '&': - case '|': - if (!*(t - 1)) { - strp2term(&b); - break; - } - *t++ = NULL; - c++->type = strp2term(&b); - c->args = t; - break; - case '`': - *t++ = ++b; - while (*b != '\'') ++b; - *b = '\0'; - break; - } - if (*(t - 1)) { - *t = NULL; - c++->type = SEMI; - } - c->args = NULL; - - return result; -} - -struct pg { - pid_t id; - struct termios config; -}; - -#define MAXPG 128 -#define INITPGS(pgs) {MAXPG, (pgs).data, (pgs).data, (pgs).data} -struct pgs { - size_t len; - struct pg *b, *c, *t, data[MAXPG]; -} spgs = INITPGS(spgs), bgpgs = INITPGS(bgpgs); - -struct pgs *recent = &spgs; - -#define PLUSONE(s, m) ((s).data + ((s).m - (s).data + 1) % (s).len) -#define INC(s, m) ((s).m = PLUSONE(s, m)) -#define MINUSONE(s, m) ((s).data + ((s).m - (s).data + (s).len - 1) % (s).len) -#define DEC(s, m) ((s).m = MINUSONE(s, m)) - -void push(struct pgs *pgs, struct pg pg) { - if (PLUSONE(*pgs, t) == pgs->b) { - killpg(pgs->b->id, SIGKILL); - INC(*pgs, b); - } - *pgs->t = pg; - INC(*pgs, t); -} - -struct pg peek(struct pgs *pgs) { - if (pgs->t == pgs->b) return (struct pg){0}; - return *MINUSONE(*pgs, t); -} - -struct pg pull(struct pgs *pgs) { - struct pg result; - - if ((result = peek(pgs)).id) DEC(*pgs, t); - return result; -} - -struct pg *find(struct pgs *pgs, pid_t pgid) { - if (pgid == 0 || pgs->t == pgs->b) return NULL; - for (pgs->c = MINUSONE(*pgs, t); pgs->c->id != pgid; DEC(*pgs, c)) - if (pgs->c == pgs->b) return NULL; - return pgs->c; -} - -void delete(struct pgs *pgs) { - if (pgs->c >= pgs->b) { - memmove(pgs->b + 1, pgs->b, sizeof(struct pg) * (pgs->c - pgs->b)); - ++pgs->b; - INC(*pgs, c); - } else { - memmove(pgs->c, pgs->c + 1, sizeof(struct pg) * (pgs->t - pgs->c - 1)); - --pgs->t; - } -} - -pid_t self; - -int waitfg(pid_t cpgid) { - int status, result; - struct pg pg; - - if (waitpid(cpgid, &status, WUNTRACED) != -1 && !WIFSTOPPED(status)) { - while (waitpid(-cpgid, NULL, 0) != -1); - if (errno != ECHILD && killpg(cpgid, SIGKILL) == -1) - err(EXIT_FAILURE, "Unable to kill child process group %d", cpgid); - } - if (signal(SIGTTOU, SIG_IGN) == SIG_ERR) - err(EXIT_FAILURE, "Unable to ignore SIGTTOU signal"); - if (tcsetpgrp(STDIN_FILENO, self) == -1) - err(EXIT_FAILURE, "Unable to set self back to foreground process group"); - if (signal(SIGTTOU, SIG_DFL) == SIG_ERR) warn("Ignoring signal SIGTTOU"); - - if (WIFSIGNALED(status)) { - putchar('\n'); - result = WTERMSIG(status); - } else if (WIFSTOPPED(status)) { - pg = (struct pg){ - .id = cpgid, - .config = canonical, - }; - if (tcgetattr(STDIN_FILENO, &pg.config) == -1) - warn("Unable to save termios state for stopped process group %d", cpgid); - push(&spgs, pg); - recent = &spgs; - result = WSTOPSIG(status); - } else result = WEXITSTATUS(status); - - if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) - warn("Unable to set termios state back to raw"); - - return result; -} - -#define BUILTINSIG(name) int name(char **tokens) - -BUILTINSIG(cd) { - if (!tokens[1]) return 1; - if (chdir(tokens[1]) != -1) return 0; - warn("Unable to change directory to `%s'", tokens[1]); - return 1; -} - -BUILTINSIG(fg) { - long id; - struct pg pg; - - if (tokens[1]) { - errno = 0; - if ((id = strtol(tokens[1], NULL, 10)) == LONG_MAX && errno || id <= 0) { - warn("Invalid process group id"); - return 1; - } - if (find(&spgs, (pid_t)id)) pg = *spgs.c; - else if (find(&bgpgs, (pid_t)id)) pg = *bgpgs.c; - else { - warn("Unable to find process group %ld", id); - return 1; - } - } else if (!(pg = peek(recent)).id) { - warnx("No processes to bring into the foreground"); - return 1; - } - if (tcsetattr(STDIN_FILENO, TCSANOW, &pg.config) == -1) { - warn("Unable to reload termios state for process group %d", pg.id); - return 1; - } - if (tcsetpgrp(STDIN_FILENO, pg.id) == -1) { - warn("Unable to bring process group %d to foreground", pg.id); - if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) - warn("Unable to set termios state back to raw mode"); - return 1; - } - if (killpg(pg.id, SIGCONT) == -1) { - if (tcsetpgrp(STDIN_FILENO, self) == -1) _Exit(EXIT_FAILURE); - warn("Unable to wake up suspended process group %d", pg.id); - if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) - warn("Unable to set termios state back to raw mode"); - return 1; - } - if (tokens[1]) delete(recent); else pull(recent); - waitfg(pg.id); - return 0; -} - -BUILTINSIG(bg) { - long id; - struct pg pg; - - if (tokens[1]) { - errno = 0; - if ((id = strtol(tokens[1], NULL, 10)) == LONG_MAX && errno || id <= 0) { - warn("Invalid process group id"); - return 1; - } - if (!find(&spgs, (pid_t)id)) { - warn("Unable to find process group %ld", id); - return 1; - } - pg = *spgs.c; - } else if (!(pg = peek(&spgs)).id) { - warnx("No suspended processes to run in the background"); - return 1; - } - if (killpg(pg.id, SIGCONT) == -1) { - warn("Unable to wake up suspended process group %d", pg.id); - return 1; - } - if (tokens[1]) delete(&spgs); else pull(&spgs); - push(&bgpgs, pg); - recent = &bgpgs; - return 0; -} - -struct builtin { - char *name; - int (*func)(char **tokens); -}; - -#define BUILTIN(name) {#name, name} - -BUILTINSIG(which); -struct builtin builtins[] = { - BUILTIN(cd), - BUILTIN(fg), - BUILTIN(bg), - BUILTIN(which), - BUILTIN(NULL), -}; - -BUILTINSIG(which) { - struct builtin *builtin; - - if (!tokens[1]) return 1; - for (builtin = builtins; builtin->func; ++builtin) - if (strcmp(tokens[1], builtin->name) == 0) { - puts("Built-in command"); - return 0; - } - // TODO: Find command in PATH - return 1; -} - -int checkbuiltin(char **args, int *statusp) { - struct builtin *builtinp; - - for (builtinp = builtins; builtinp->func; ++builtinp) - if (strcmp(*args, builtinp->name) == 0) { - *statusp = builtinp->func(args); - return 1; - } - return 0; -} - -void waitbg(int sig) { - int status; - pid_t pid, pgid; - - (void)sig; - for (bgpgs.c = bgpgs.b; bgpgs.c != bgpgs.t; INC(bgpgs, c)) - do switch ((pid = waitpid(-bgpgs.c->id, &status, WNOHANG | WUNTRACED))) { - case -1: - if (errno != ECHILD) warn("Unable to wait on some child processes"); - case 0: - break; - default: - if (WIFSTOPPED(status)) { - push(&spgs, *bgpgs.c); - delete(&bgpgs); - recent = &spgs; - pid = 0; - } - } while (pid > 0); -} - -int main(void) { - char *input; - struct cmd *cmds; - struct builtin *builtin; - pid_t pipegpid, cpid, id; - int status, cpipe[2], ppipe[2]; - - readhist(); - if (atexit(writehist) == -1) - err(EXIT_FAILURE, "Unable to add `writehist()' to `atexit()'"); - - if (tcgetattr(STDIN_FILENO, &canonical) == -1) - err(EXIT_FAILURE, "Unable to get termios structure"); - raw = canonical; - raw.c_lflag &= ~ICANON & ~ECHO & ~ISIG; - raw.c_lflag |= ECHONL; - if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) - err(EXIT_FAILURE, "Unable to initialize termios structure"); - if (atexit(ashexit) == -1) - err(EXIT_FAILURE, "Unable to add `ashexit()' to `atexit()'"); - - if ((self = getpgid(0)) == -1) err(EXIT_FAILURE, "Unable to get pgid of self"); - for (pipegpid = 0;; waitbg(0)) { - signal(SIGCHLD, waitbg); - if (!(input = getinput())) { - signal(SIGCHLD, SIG_DFL); // TODO: Make this not repeated... - continue; - } - signal(SIGCHLD, SIG_DFL); // ...with here - ppipe[1] = ppipe[0] = 0; - for (cmds = getcmds(input); cmds->args; ++cmds) { - if (cmds->type == PIPE) { - if (pipe(cpipe) == -1) warn("Unable to create pipe"); - if ((cpid = fork()) == 0) { - if (dup2(ppipe[0], 0) == -1) warn("Unable to read from pipe"); - if (dup2(cpipe[1], 1) == -1) warn("Unable to write to pipe"); - close(ppipe[0]); - close(ppipe[1]); - close(cpipe[0]); - close(cpipe[1]); - if (checkbuiltin(cmds->args, &status)) _Exit(EXIT_SUCCESS); - if (execvp(*cmds->args, cmds->args) == -1) { - warn("Unable to exec `%s' in pipe", *cmds->args); - _Exit(EXIT_FAILURE); - } - } - if (ppipe[0] && close(ppipe[0]) == -1) - warn("Unable to close read end of previous pipe"); - if (ppipe[1] && close(ppipe[1]) == -1) - warn("Unable to close write end of previous pipe"); - ppipe[0] = cpipe[0]; - ppipe[1] = cpipe[1]; - if (!pipegpid) { - pipegpid = cpid; - push(&bgpgs, (struct pg){.id = pipegpid, .config = canonical,}); - } - setpgid(cpid, pipegpid); - continue; - } else { - if (pipegpid || !checkbuiltin(cmds->args, &status)) { - if ((cpid = fork()) == 0) { - if (pipegpid) { - if (dup2(ppipe[0], 0) == -1) { - warn("Unable to read from pipe"); - _Exit(EXIT_FAILURE); - } - close(ppipe[0]); - close(ppipe[1]); - if (checkbuiltin(cmds->args, &status)) _Exit(EXIT_SUCCESS); - } - if (execvp(*cmds->args, cmds->args) == -1) { - warn("Unable to exec `%s'", *cmds->args); - _Exit(EXIT_FAILURE); - } - } - if (pipegpid) { - id = pipegpid; - close(ppipe[0]); - close(ppipe[1]); - } else id = cpid; - setpgid(cpid, id); - if (cmds->type == BG) recent = &bgpgs; - else { - if (tcsetattr(STDIN_FILENO, TCSANOW, &canonical) == -1) - warn("Unable to set termios structure"); - if (tcsetpgrp(STDIN_FILENO, id) == -1) { - warn("Unable to bring process group %d to foreground", id); - if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) - warn("Unable to set termios state back to raw"); - if (killpg(id, SIGKILL) == -1) - warn("Unable to kill process group %d; manual termination required", id); - break; - } - // if (killpg(id, SIGCONT) == -1) { // ?? - // if (tcsetpgrp(STDIN_FILENO, self) == -1) _Exit(EXIT_FAILURE); - // warn("Process group %d may be blocked; killing it", id); - // if (killpg(id, SIGKILL) == -1) - // warn("Unable to kill process group %d; manual termination required", id); - // else warn("Successfully terminated process group %d", id); - // if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) - // warn("Unable to set termios state back to raw"); - // break; - // } - if (pipegpid) pull(&bgpgs); - status = waitfg(id); - } - } - if (cmds->type == AND && status != 0) break; - if (cmds->type == OR && status == 0) break; - if (pipegpid) pipegpid = 0; - } - } - } - return 0; -} diff --git a/src/builtins.c b/src/builtins.c new file mode 100644 index 0000000..5edcdf8 --- /dev/null +++ b/src/builtins.c @@ -0,0 +1,133 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pg.h" +#include "term.h" +#include "wait.h" + +#define BUILTINSIG(name) int name(char **tokens) + +BUILTINSIG(cd) { + if (!tokens[1]) return 1; + if (chdir(tokens[1]) != -1) return 0; + warn("Unable to change directory to `%s'", tokens[1]); + return 1; +} + +BUILTINSIG(fg) { + long id; + struct pg pg; + + if (tokens[1]) { + errno = 0; + if ((id = strtol(tokens[1], NULL, 10)) == LONG_MAX && errno || id <= 0) { + warn("Invalid process group id"); + return 1; + } + if (find(&spgs, (pid_t)id)) pg = *spgs.c; + else if (find(&bgpgs, (pid_t)id)) pg = *bgpgs.c; + else { + warn("Unable to find process group %ld", id); + return 1; + } + } else if (!(pg = peek(recent)).id) { + warnx("No processes to bring into the foreground"); + return 1; + } + if (tcsetattr(STDIN_FILENO, TCSANOW, &pg.config) == -1) { + warn("Unable to reload termios state for process group %d", pg.id); + return 1; + } + if (tcsetpgrp(STDIN_FILENO, pg.id) == -1) { + warn("Unable to bring process group %d to foreground", pg.id); + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) + warn("Unable to set termios state back to raw mode"); + return 1; + } + if (killpg(pg.id, SIGCONT) == -1) { + if (tcsetpgrp(STDIN_FILENO, self) == -1) _Exit(EXIT_FAILURE); + warn("Unable to wake up suspended process group %d", pg.id); + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) + warn("Unable to set termios state back to raw mode"); + return 1; + } + if (tokens[1]) cut(recent); else pull(recent); + waitfg(pg.id); + return 0; +} + +BUILTINSIG(bg) { + long id; + struct pg pg; + + if (tokens[1]) { + errno = 0; + if ((id = strtol(tokens[1], NULL, 10)) == LONG_MAX && errno || id <= 0) { + warn("Invalid process group id"); + return 1; + } + if (!find(&spgs, (pid_t)id)) { + warn("Unable to find process group %ld", id); + return 1; + } + pg = *spgs.c; + } else if (!(pg = peek(&spgs)).id) { + warnx("No suspended processes to run in the background"); + return 1; + } + if (killpg(pg.id, SIGCONT) == -1) { + warn("Unable to wake up suspended process group %d", pg.id); + return 1; + } + if (tokens[1]) cut(&spgs); else pull(&spgs); + push(&bgpgs, pg); + recent = &bgpgs; + return 0; +} + +struct builtin { + char *name; + int (*func)(char **tokens); +}; + +#define BUILTIN(name) {#name, name} + +BUILTINSIG(which); +struct builtin builtins[] = { + BUILTIN(cd), + BUILTIN(fg), + BUILTIN(bg), + BUILTIN(which), + BUILTIN(NULL), +}; + +BUILTINSIG(which) { + struct builtin *builtin; + + if (!tokens[1]) return 1; + for (builtin = builtins; builtin->func; ++builtin) + if (strcmp(tokens[1], builtin->name) == 0) { + puts("Built-in command"); + return 0; + } + // TODO: Find command in PATH + return 1; +} + +int isbuiltin(char **args, int *statusp) { + struct builtin *builtinp; + + for (builtinp = builtins; builtinp->func; ++builtinp) + if (strcmp(*args, builtinp->name) == 0) { + *statusp = builtinp->func(args); + return 1; + } + return 0; +} diff --git a/src/builtins.h b/src/builtins.h new file mode 100644 index 0000000..0798e58 --- /dev/null +++ b/src/builtins.h @@ -0,0 +1 @@ +int isbuiltin(char **args, int *statusp); diff --git a/src/cmd.c b/src/cmd.c new file mode 100644 index 0000000..74e536f --- /dev/null +++ b/src/cmd.c @@ -0,0 +1,75 @@ +#include + +#include "history.h" +#include "cmd.h" + +int delimiter(char c) { + return c == ' ' || c == '&' || c == '|' || c == ';' || c == '`' || c == '\0'; +} + +enum terminator strp2term(char **strp) { + enum terminator result; + char *p; + + switch (*(p = (*strp)++)) { + case '&': + result = **strp == '&' ? (++*strp, AND) : BG; + break; + case '|': + result = **strp == '|' ? (++*strp, OR) : PIPE; + break; + default: + result = SEMI; + } + *p = '\0'; + + return result; +} + +struct cmd *getcmds(char *b) { + size_t i, j; + char **t; + struct cmd *c; + enum terminator term; + static char *tokens[BUFLEN + 1]; + static struct cmd result[BUFLEN / 2 + 1]; + + *tokens = NULL; + t = tokens + 1; + c = result; + while (*b == ' ') ++b; + c->args = t; + while (*b) switch (*b) { + default: + *t++ = b; + while (!delimiter(*b)) ++b; + break; + case ' ': + *b++ = '\0'; + while (*b == ' ') ++b; + break; + case ';': + case '&': + case '|': + if (!*(t - 1)) { + strp2term(&b); + break; + } + *t++ = NULL; + c++->type = strp2term(&b); + c->args = t; + break; + case '`': + *t++ = ++b; + while (*b != '\'') ++b; + *b = '\0'; + break; + } + if (*(t - 1)) { + *t = NULL; + c++->type = SEMI; + } + *c = (struct cmd){0}; + + return result; +} diff --git a/src/cmd.h b/src/cmd.h new file mode 100644 index 0000000..82850f3 --- /dev/null +++ b/src/cmd.h @@ -0,0 +1,15 @@ +enum terminator { + SEMI, + BG, + AND, + PIPE, + OR, +}; + +struct cmd { + char **args; + enum terminator type; + int pipe[2]; +}; + +struct cmd *getcmds(char *b); diff --git a/src/history.c b/src/history.c new file mode 100644 index 0000000..1509e66 --- /dev/null +++ b/src/history.c @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include + +#include "input.h" +#include "utils.h" +#include "history.h" + +#define HISTNAME ".ashhistory" + +// TODO: This could be its own struct and use macros like the stopped process table +char history[HISTLEN][BUFLEN], (*hb)[BUFLEN], (*hc)[BUFLEN], (*ht)[BUFLEN]; +static char *histpath; + +void readhist(void) { + char *homepath; + FILE *histfile; + int e; + + if (!(homepath = getenv("HOME"))) + errx(EXIT_FAILURE, "HOME environment variable does not exist"); + histpath = allocate(strlen(homepath) + 1 + strlen(HISTNAME) + 1); + strcpy(histpath, homepath); + strcat(histpath, "/"); + strcat(histpath, HISTNAME); + + ht = hc = hb = history; + if (!(histfile = fopen(histpath, "r"))) { + if (errno == ENOENT) return; + err(EXIT_FAILURE, "Unable to open history file for reading"); + } + while (fgets(*ht, BUFLEN, histfile)) { + *(*ht + strlen(*ht) - 1) = '\0'; // Get rid of newline + ht = history + (ht - history + 1) % HISTLEN; + if (ht == hb) hb = NULL; + } + + e = ferror(histfile) || !feof(histfile); + if (fclose(histfile) == EOF) err(EXIT_FAILURE, "Unable to close history file"); + if (e) err(EXIT_FAILURE, "Unable to read from history file"); + + if (!hb) hb = history + (ht - history + 1) % HISTLEN; + hc = ht; +} + +void writehist(void) { + FILE *histfile; + + if (!(histfile = fopen(histpath, "w"))) { + warn("Unable to open history file for writing"); + return; + } + + while (hb != ht) { + if (fputs(*hb, histfile) == EOF) { + warn("Unable to write to history file"); + break; + } + if (fputc('\n', histfile) == EOF) { + warn("Unable to write newline to history file"); + break; + } + hb = history + (hb - history + 1) % HISTLEN; + } + + if (fclose(histfile) == EOF) warn("Unable to close history stream"); +} diff --git a/src/history.h b/src/history.h new file mode 100644 index 0000000..54e558f --- /dev/null +++ b/src/history.h @@ -0,0 +1,7 @@ +#define HISTLEN 101 +#define BUFLEN 1001 + +extern char history[HISTLEN][BUFLEN], (*hb)[BUFLEN], (*hc)[BUFLEN], (*ht)[BUFLEN]; + +void readhist(void); +void writehist(void); diff --git a/src/input.c b/src/input.c new file mode 100644 index 0000000..134c578 --- /dev/null +++ b/src/input.c @@ -0,0 +1,112 @@ +#include +#include +#include +#include +#include + +#include "history.h" +#include "pg.h" + +#define PROMPT "% " + +enum { + CTRLC = '\003', + CTRLD, + ESCAPE = '\033', + UP = 'A', + DOWN, + RIGHT, + LEFT, + BACKSPACE = '\177', +}; + +int getinput(char *buffer) { + char *cursor, *end; + int c, i; + + signal(SIGCHLD, waitbg); + + end = cursor = buffer; + *buffer = '\0'; + while (buffer == end) { + fputs(PROMPT, stdout); + while ((c = getchar()) != '\n') { + if (c >= ' ' && c <= '~') { + if (end - buffer == BUFLEN - 1) continue; + memmove(cursor + 1, cursor, end - cursor); + *cursor++ = c; + *++end = '\0'; + + putchar(c); + fputs(cursor, stdout); + for (i = end - cursor; i > 0; --i) putchar('\b'); + } else switch (c) { + case CTRLC: + puts("^C"); + ungetc('\n', stdin); + continue; + case CTRLD: + puts("^D"); + signal(SIGCHLD, SIG_DFL); + return 0; + case ESCAPE: + if ((c = getchar()) != '[') { + ungetc(c, stdin); + break; + } + switch ((c = getchar())) { + case UP: + case DOWN: + if (hc == (c == UP ? hb : ht)) continue; + if (strcmp(*hc, buffer) != 0) strcpy(*ht, buffer); + + putchar('\r'); + for (i = end - buffer + strlen(PROMPT); i > 0; --i) putchar(' '); + putchar('\r'); + fputs(PROMPT, stdout); + + hc = history + ((hc - history + (c == UP ? -1 : 1)) % HISTLEN + HISTLEN) + % HISTLEN; + strcpy(buffer, *hc); + end = cursor = buffer + strlen(buffer); + + fputs(buffer, stdout); + break; + case LEFT: + if (cursor > buffer) { + putchar('\b'); + --cursor; + } + break; + case RIGHT: + if (cursor < end) putchar(*cursor++); + break; + } + break; + case BACKSPACE: + if (cursor == buffer) continue; + memmove(cursor - 1, cursor, end - cursor); + --cursor; + *--end = '\0'; + + putchar('\b'); + fputs(cursor, stdout); + putchar(' '); + for (i = end - cursor + 1; i > 0; --i) putchar('\b'); + + break; + } + } + } + + fpurge(stdout); + strcpy(*ht, buffer); + ht = history + (ht - history + 1) % HISTLEN; + hc = ht; + if (ht == hb) hb = history + (hb - history + 1) % HISTLEN; + + signal(SIGCHLD, SIG_DFL); + + return 1; +} + diff --git a/src/input.h b/src/input.h new file mode 100644 index 0000000..6e39fb1 --- /dev/null +++ b/src/input.h @@ -0,0 +1 @@ +int getinput(char *buffer); diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..5906e06 --- /dev/null +++ b/src/main.c @@ -0,0 +1,113 @@ +#include +#include // TODO: Use sigaction for portability? but this is also used with killpg, so don't remove it +#include +#include +#include +#include +#include + +#include "builtins.h" +#include "cmd.h" +#include "history.h" +#include "input.h" +#include "pg.h" +#include "term.h" +#include "utils.h" + +int closepipe(struct cmd *cmd) { + int result; + + if (!cmd->args) return 1; + + result = 1; + if (close(cmd->pipe[0]) == -1) { + warn("Unable to close read end of `%s' pipe", *cmd->args); + result = 0; + } + if (close(cmd->pipe[1]) == -1) { + warn("Unable to close write end of `%s' pipe", *cmd->args); + result = 0; + } + return result; +} + +int main(void) { + char buffer[BUFLEN]; + struct cmd *cmd, *prev; + int status; + pid_t id; + + initterm(); + readhist(); + + while (getinput(buffer)) { + prev = cmd = getcmds(buffer); + while (prev->args) ++prev; + while (cmd->args) { + if (cmd->type != PIPE && prev->type != PIPE) { + if (isbuiltin(cmd->args, &status)) break; + if ((id = fork()) == 0 && execvp(*cmd->args, cmd->args) == -1) + err(EXIT_FAILURE, "Couldn't find `%s' command", *cmd->args); + if (setpgid(id, id) == -1) { + warn("Unable to set pgid of `%s' command to %d", *cmd->args, id); + if (kill(id, SIGKILL) == -1) + warn("Unable to kill process %d; manual termination may be required", + id); + break; + } + } else { + if (cmd->type == PIPE && pipe(cmd->pipe) == -1) { + warn("Unable to create pipe"); + if (prev->type == PIPE) closepipe(prev); + break; + } + if ((id = fork()) == 0) { + if (dup2(prev->pipe[0], 0) == -1) + err(EXIT_FAILURE, "Unable to duplicate read end of `%s' pipe", + *prev->args); + if (!closepipe(prev)) exit(EXIT_FAILURE); + + if (cmd->type == PIPE) { + if (dup2(cmd->pipe[1], 1) == -1) + err(EXIT_FAILURE, "Unable to duplicate write end of `%s' pipe", + *cmd->args); + if (!closepipe(cmd)) exit(EXIT_FAILURE); + } + + if (isbuiltin(cmd->args, &status)) exit(EXIT_SUCCESS); + if (execvp(*cmd->args, cmd->args) == -1) + err(EXIT_FAILURE, "Couldn't find `%s' command", *cmd->args); + } + if (prev->type == PIPE) closepipe(prev); + else push(&bgpgs, (struct pg){.id = id, .config = canonical,}); + if (setpgid(id, peek(&bgpgs).id) == -1) { + if (errno != ESRCH) { + warn("Unable to set pgid of `%s' command to %d", *cmd->args, peek(&bgpgs).id); + if (kill(id, SIGKILL) == -1) + warn("Unable to kill process %d; manual termination may be required", + id); + } + break; + } + if (cmd->type != PIPE) id = peek(&bgpgs).id; + } + if (cmd->type == BG) { + push(&bgpgs, (struct pg){.id = id, .config = canonical,}); + recent = &bgpgs; + } else if (cmd->type != PIPE) { + if (prev->type == PIPE) pull(&bgpgs); + if (!setfg(id)) break; + status = waitfg(id); + } + if (cmd->type == AND && status != 0) break; + if (cmd->type == OR && status == 0) break; + prev = cmd++; + } + waitbg(0); + } + + writehist(); + deinitterm(); + + return 0; +} diff --git a/src/pg.c b/src/pg.c new file mode 100644 index 0000000..ec3cedf --- /dev/null +++ b/src/pg.c @@ -0,0 +1,74 @@ +#include +#include +#include +#include +#include +#include + +#include "pg.h" + +#define INITPGS(pgs) {MAXPG, (pgs).data, (pgs).data, (pgs).data} +struct pgs spgs = INITPGS(spgs), bgpgs = INITPGS(bgpgs), *recent = &spgs; + +void sigkill(pid_t pgid) { + if (killpg(pgid, SIGKILL) == -1) + warn("Unable to kill process group %d; manual termination may be required", + pgid); +} + +void push(struct pgs *pgs, struct pg pg) { + if (PLUSONE(*pgs, t) == pgs->b) { + sigkill(pgs->b->id); + INC(*pgs, b); + } + *pgs->t = pg; + INC(*pgs, t); +} + +struct pg peek(struct pgs *pgs) { + if (pgs->t == pgs->b) return (struct pg){0}; + return *MINUSONE(*pgs, t); +} + +struct pg pull(struct pgs *pgs) { + struct pg result; + + if ((result = peek(pgs)).id) DEC(*pgs, t); + return result; +} + +struct pg *find(struct pgs *pgs, pid_t pgid) { + if (pgid == 0 || pgs->t == pgs->b) return NULL; + for (pgs->c = MINUSONE(*pgs, t); pgs->c->id != pgid; DEC(*pgs, c)) + if (pgs->c == pgs->b) return NULL; + return pgs->c; +} + +void cut(struct pgs *pgs) { + if (pgs->c >= pgs->b) { + memmove(pgs->b + 1, pgs->b, sizeof(struct pg) * (pgs->c - pgs->b)); + ++pgs->b; + INC(*pgs, c); + } else { + memmove(pgs->c, pgs->c + 1, sizeof(struct pg) * (pgs->t - pgs->c - 1)); + --pgs->t; + } +} + +void waitbg(int sig) { + int status; + pid_t pid, pgid; + + (void)sig; + for (bgpgs.c = bgpgs.b; bgpgs.c != bgpgs.t; INC(bgpgs, c)) { + while ((pid = waitpid(-bgpgs.c->id, &status, WNOHANG | WUNTRACED)) > 0) + if (WIFSTOPPED(status)) { + push(&spgs, *bgpgs.c); + cut(&bgpgs); + recent = &spgs; + pid = 0; + } + if (pid == -1 && errno != ECHILD) + warn("Unable to wait on some child processes"); + } +} diff --git a/src/pg.h b/src/pg.h new file mode 100644 index 0000000..522f958 --- /dev/null +++ b/src/pg.h @@ -0,0 +1,25 @@ +struct pg { + pid_t id; + struct termios config; +}; + +#define PLUSONE(s, m) ((s).data + ((s).m - (s).data + 1) % (s).len) +#define INC(s, m) ((s).m = PLUSONE(s, m)) +#define MINUSONE(s, m) ((s).data + ((s).m - (s).data + (s).len - 1) % (s).len) +#define DEC(s, m) ((s).m = MINUSONE(s, m)) + +#define MAXPG 128 +struct pgs { + size_t len; + struct pg *b, *c, *t, data[MAXPG]; +}; + +extern struct pgs spgs, bgpgs, *recent; + +void sigkill(pid_t pgid); +void push(struct pgs *pgs, struct pg pg); +struct pg peek(struct pgs *pgs); +struct pg pull(struct pgs *pgs); +struct pg *find(struct pgs *pgs, pid_t pgid); +void cut(struct pgs *pgs); +void waitbg(int sig); diff --git a/src/term.c b/src/term.c new file mode 100644 index 0000000..a2bbe1c --- /dev/null +++ b/src/term.c @@ -0,0 +1,80 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "pg.h" + +struct termios canonical, raw; +pid_t self; + +void initterm(void) { + if (tcgetattr(STDIN_FILENO, &canonical) == -1) + err(EXIT_FAILURE, "Unable to get termios structure"); + raw = canonical; + raw.c_lflag &= ~ICANON & ~ECHO & ~ISIG; + raw.c_lflag |= ECHONL; + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) + err(EXIT_FAILURE, "Unable to initialize termios structure"); + + if ((self = getpgid(0)) == -1) err(EXIT_FAILURE, "Unable to get pgid of self"); +} + +void deinitterm(void) { + if (tcsetattr(STDIN_FILENO, TCSANOW, &canonical) == -1) + warn("Unable to return to initial terminal settings"); +} + +int setfg(pid_t pgid) { + if (tcsetattr(STDIN_FILENO, TCSANOW, &canonical) == -1) + warn("Unable to set termios structure"); + if (tcsetpgrp(STDIN_FILENO, pgid) == -1) { + warn("Unable to bring process group %d to foreground", pgid); + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) + warn("Unable to set termios state back to raw"); + sigkill(pgid); + if (killpg(pgid, SIGKILL) == -1) + warn("Unable to kill process group %d; manual termination required", pgid); + return 0; + } + return 1; +} + +int waitfg(pid_t pgid) { + int status, result; + struct pg pg; + + if (waitpid(pgid, &status, WUNTRACED) != -1 && !WIFSTOPPED(status)) { + while (waitpid(-pgid, NULL, 0) != -1); + if (errno != ECHILD && killpg(pgid, SIGKILL) == -1) + err(EXIT_FAILURE, "Unable to kill child process group %d", pgid); + } + if (signal(SIGTTOU, SIG_IGN) == SIG_ERR) + err(EXIT_FAILURE, "Unable to ignore SIGTTOU signal"); + if (tcsetpgrp(STDIN_FILENO, self) == -1) + err(EXIT_FAILURE, "Unable to set self back to foreground process group"); + if (signal(SIGTTOU, SIG_DFL) == SIG_ERR) warn("Ignoring signal SIGTTOU"); + + if (WIFSIGNALED(status)) { + putchar('\n'); + result = WTERMSIG(status); + } else if (WIFSTOPPED(status)) { + pg = (struct pg){ + .id = pgid, + .config = canonical, + }; + if (tcgetattr(STDIN_FILENO, &pg.config) == -1) + warn("Unable to save termios state for stopped process group %d", pgid); + push(&spgs, pg); + recent = &spgs; + result = WSTOPSIG(status); + } else result = WEXITSTATUS(status); + + if (tcsetattr(STDIN_FILENO, TCSANOW, &raw) == -1) + warn("Unable to set termios state back to raw"); + + return result; +} diff --git a/src/term.h b/src/term.h new file mode 100644 index 0000000..0603b8e --- /dev/null +++ b/src/term.h @@ -0,0 +1,7 @@ +extern struct termios canonical, raw; +extern pid_t self; + +void initterm(void); +void deinitterm(void); +int setfg(pid_t pgid); +int waitfg(pid_t pgid); diff --git a/src/utils.c b/src/utils.c new file mode 100644 index 0000000..07327e4 --- /dev/null +++ b/src/utils.c @@ -0,0 +1,12 @@ +#include +#include +#include + +void *allocate(size_t s) { + void *r; + + if (!(r = malloc(s))) err(EXIT_FAILURE, "Memory allocation"); + + return memset(r, 0, s); +} + diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..8954d59 --- /dev/null +++ b/src/utils.h @@ -0,0 +1 @@ +void *allocate(size_t s);