/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│ vi: set et ft=c ts=2 sts=2 sw=2 fenc=utf-8                               :vi │
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2022 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/calls/struct/stat.h"
#include "libc/calls/struct/timespec.h"
#include "libc/ctype.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/fmt/conv.h"
#include "libc/fmt/itoa.h"
#include "libc/fmt/magnumstrs.internal.h"
#include "libc/intrin/getenv.h"
#include "libc/intrin/weaken.h"
#include "libc/limits.h"
#include "libc/macros.h"
#include "libc/runtime/runtime.h"
#include "libc/serialize.h"
#include "libc/stdio/stdio.h"
#include "libc/str/str.h"
#include "libc/sysv/consts/lock.h"
#include "libc/sysv/consts/o.h"
#include "libc/sysv/consts/ok.h"
#include "libc/sysv/consts/s.h"
#include "libc/sysv/consts/sig.h"
#include "libc/sysv/consts/timer.h"
#include "libc/temp.h"
#include "third_party/awk/cmd.h"
#include "third_party/getopt/getopt.internal.h"
#include "third_party/musl/glob.h"
#include "third_party/sed/cmd.h"
#include "third_party/tr/cmd.h"
#include "tool/curl/cmd.h"

/**
 * @fileoverview Cosmopolitan Command Interpreter
 *
 * This is a lightweight command interpreter for GNU Make. It has just
 * enough shell script language support to support our build config.
 */

#define STATE_CMD        0
#define STATE_VAR        1
#define STATE_SINGLE     2
#define STATE_QUOTED     3
#define STATE_QUOTED_VAR 4
#define STATE_WHITESPACE 5

#define TOMBSTONE ((char *)-1)
#define READ24(s) READ32LE(s "\0")

static char *p;
static char *q;
static char *r;
static int vari;
static size_t n;
static char *cmd;
static char *assign;
static char var[32];
static int lastchild;
static int exitstatus;
static char *envs[500];
static char *args[3000];
static const char *prog;
static char argbuf[ARG_MAX];
static bool unsupported[256];

static int ShellSpawn(void);
static int ShellExec(void);

static ssize_t Write(int fd, const char *s) {
  return write(fd, s, strlen(s));
}

static wontreturn void UnsupportedSyntax(unsigned char c) {
  char ibuf[13], cbuf[2] = {c};
  FormatOctal32(ibuf, c, true);
  tinyprint(2, prog, ": unsupported syntax '", cbuf, "' (", ibuf, "): ", cmd,
            "\n", NULL);
  _Exit(4);
}

static void Open(const char *path, int fd, int flags) {
  int tmpfd;
  // use open+dup2 to support things like >/dev/stdout
  if ((tmpfd = open(path, flags, 0644)) == -1) {
    perror(path);
    _Exit(1);
  }
  if (tmpfd != fd) {
    if (dup2(tmpfd, fd) == -1) {
      perror("dup2");
      _Exit(1);
    }
    close(tmpfd);
  }
}

static int SystemExec(void) {
  execvpe(args[0], args, envs);
  perror(args[0]);
  return (n = 0), 127;
}

static int GetSignalByName(const char *s) {
  for (int i = 0; kSignalNames[i].x != MAGNUM_TERMINATOR; ++i) {
    if (!strcmp(s, MAGNUM_STRING(kSignalNames, i) + 3)) {
      return MAGNUM_NUMBER(kSignalNames, i);
    }
  }
  return 0;
}

static void PutEnv(char **p, char *kv) {
  struct Env e;
  e = __getenv(p, kv);
  p[e.i] = kv;
  if (!e.s)
    p[e.i + 1] = 0;
}

static void UnsetEnv(char **p, const char *k) {
  int i;
  struct Env e;
  if ((e = __getenv(p, k)).s) {
    p[e.i] = 0;
    for (i = e.i + 1; p[i]; ++i) {
      p[i - 1] = p[i];
      p[i] = 0;
    }
  }
}

static void Append(int c) {
  npassert(q + 1 < argbuf + sizeof(argbuf));
  *q++ = c;
}

static char *Finish(void) {
  char *s = r;
  Append(0);
  r = q;
  if (!assign) {
    return s;
  } else {
    PutEnv(envs, s);
    assign = 0;
    return TOMBSTONE;
  }
}

static int Pause(void) {
  pause();
  return 0;
}

static int True(void) {
  return 0;
}

static int False(void) {
  return 1;
}

static wontreturn void Exit(void) {
  _Exit(n > 1 ? atoi(args[1]) : 0);
}

static int Waiter(int pid) {
  int ws;
  n = 0;
  if (waitpid(pid, &ws, 0) == -1) {
    perror("wait");
    return 1;
  }
  if (WIFEXITED(ws)) {
    return WEXITSTATUS(ws);
  } else {
    return 128 + WTERMSIG(ws);
  }
}

static int Wait(void) {
  int e, ws, pid;
  if (n > 1) {
    if ((pid = atoi(args[1])) <= 0) {
      tinyprint(2, "wait: bad pid\n", NULL);
      return 1;
    }
    return Waiter(pid);
  } else {
    for (n = 0, e = errno;;) {
      if (waitpid(-1, &ws, 0) == -1) {
        if (errno == ECHILD) {
          errno = e;
          break;
        }
        perror("wait");
        return 1;
      }
    }
    return 0;
  }
}

static const char *GetOptArg(int c, int *i, int j) {
  if (args[*i][j] == c) {
    if (args[*i][j + 1]) {
      return args[*i] + j + 1;
    } else if (*i + 1 < n) {
      return args[++*i];
    }
  }
  return 0;
}

static int Echo(void) {
  int i = 1;
  bool once = false;
  bool print_newline = true;
  if (i < n && args[i][0] == '-' && args[i][1] == 'n' && !args[i][2]) {
    ++i, print_newline = false;
  }
  for (; i < n; ++i) {
    if (once) {
      Write(1, " ");
    } else {
      once = true;
    }
    Write(1, args[i]);
  }
  if (print_newline) {
    Write(1, "\n");
  }
  return 0;
}

static int NeedArgument(const char *prog) {
  tinyprint(2, prog, ": missing argument\n", NULL);
  return 1;
}

static int Flock(void) {
  int fd;
  int i = 1;
  char *endptr;
  int mode = LOCK_EX;
  if (i < n && args[i][0] == '-' && args[i][1] == 'x' && !args[i][2]) {
    ++i, mode = LOCK_EX;
  }
  if (i < n && args[i][0] == '-' && args[i][1] == 's' && !args[i][2]) {
    ++i, mode = LOCK_SH;
  }
  if (i == n) {
    return NeedArgument("flock");
  }
  if (i + 1 != n) {
    tinyprint(2, "flock: too many arguments\n", NULL);
    return 1;
  }
  fd = strtol(args[i], &endptr, 10);
  if (*endptr) {
    tinyprint(2, "flock: need a number\n", NULL);
    return 1;
  }
  if (flock(fd, mode)) {
    perror("flock");
    return 1;
  }
  return 0;
}

static int Chmod(void) {
  int i, mode;
  char *endptr;
  if (n < 3) {
    return NeedArgument("chmod");
  }
  mode = strtol(args[1], &endptr, 8);
  if (*endptr) {
    tinyprint(2, "chmod: bad octal mode\n", NULL);
    return 1;
  }
  for (i = 2; i < n; ++i) {
    if (chmod(args[i], mode)) {
      perror(args[i]);
      return 1;
    }
  }
  return 0;
}

static int Pwd(void) {
  int got;
  char path[PATH_MAX];
  if ((got = __getcwd(path, PATH_MAX - 1)) != -1) {
    path[got - 1] = '\n';
    path[got] = 0;
    Write(1, path);
    return 0;
  } else {
    perror("pwd");
    return 1;
  }
}

static int CatDump(const char *path, int fd, bool dontclose) {
  ssize_t rc;
  char buf[512];
  for (;;) {
    rc = read(fd, buf, sizeof(buf));
    if (rc == -1) {
      perror(path);
      if (!dontclose) {
        close(fd);
      }
      return 1;
    }
    if (!rc)
      break;
    rc = write(1, buf, rc);
    if (rc == -1) {
      perror("write");
      if (!dontclose) {
        close(fd);
      }
      return 1;
    }
  }
  if (!dontclose && close(fd)) {
    perror(path);
    return 1;
  }
  return 0;
}

static int Cat(void) {
  int i, fd, rc;
  if (n < 2) {
    return CatDump("<stdin>", 0, true);
  } else {
    for (i = 1; i < n; ++i) {
      bool dontclose = false;
      if (args[i][0] == '-' && !args[i][1]) {
        dontclose = true;
        fd = 0;
      } else if ((fd = open(args[i], O_RDONLY)) == -1) {
        perror(args[i]);
        return 1;
      }
      if ((rc = CatDump(args[i], fd, dontclose))) {
        return rc;
      }
    }
  }
  return 0;
}

static int Mktemp(void) {
  int fd;
  char template[PATH_MAX + 1];
  if (n == 2) {
    strlcpy(template, args[1], sizeof(template));
  } else {
    strlcpy(template, __get_tmpdir(), sizeof(template));
    strlcat(template, "tmp.XXXXXX", sizeof(template));
  }
  if ((fd = mkstemp(template)) == -1) {
    perror("mkstemp");
    return 1;
  }
  strlcat(template, "\n", sizeof(template));
  Write(1, template);
  close(fd);
  return 0;
}

static int Read(void) {
  unsigned char c;
  int i, j, rc = 1;
  for (i = 1; i < n; ++i) {
    if (args[i][0] != '-')
      break;
    if (args[i][1] == 'p' && !args[i][2] && i + 1 < n) {
      Write(1, args[++i]);
    }
  }
  if (i >= n)
    return 1;
  for (j = 0; args[i][j]; ++j) {
    Append(args[i][j]);
  }
  Append('=');
  while (read(0, &c, 1) > 0) {
    if (c == '\n')
      break;
    Append(c);
    rc = 0;
  }
  PutEnv(envs, Finish());
  return rc;
}

static int Cd(void) {
  const char *s;
  if ((s = n > 1 ? args[1] : __getenv(envs, "HOME").s)) {
    if (!chdir(s)) {
      return 0;
    } else {
      perror(s);
      return 1;
    }
  } else {
    return NeedArgument("cd");
  }
}

static int Mkdir(void) {
  int i, j;
  int mode = 0755;
  const char *arg;
  int (*mkdir_impl)(const char *, unsigned) = mkdir;
  for (i = 1; i < n; ++i) {
    if (args[i][0] == '-' && args[i][1]) {
      if (args[i][1] == '-' && !args[i][2]) {
        ++i;  // rm -- terminates option parsing
        break;
      }
      for (j = 1; j < args[i][j]; ++j) {
        if (args[i][j] == 'p') {
          mkdir_impl = makedirs;  // mkdir -p creates parents
          continue;
        }
        if ((arg = GetOptArg('m', &i, j))) {
          mode = strtol(arg, 0, 8);  // mkdir -m OCTAL sets mode
          break;
        }
        char option[2] = {args[i][j]};
        tinyprint(2, "mkdir", ": illegal option -- ", option, "\n", NULL);
        return 1;
      }
    } else {
      break;
    }
  }
  if (i == n) {
    return NeedArgument("mkdir");
  }
  for (; i < n; ++i) {
    if (mkdir_impl(args[i], mode)) {
      perror(args[i]);
      return 1;
    }
  }
  return 0;
}

static int Kill(void) {
  int sig, rc = 0, i = 1;
  if (i < n && args[i][0] == '-') {
    sig = GetSignalByName(args[i++] + 1);
    if (!sig)
      return -1;  // fallback to system kill command
  } else {
    sig = SIGTERM;
  }
  for (; i < n; ++i) {
    if (kill(atoi(args[i]), sig)) {
      perror("kill");
      rc = 1;
    }
  }
  return rc;
}

static int Toupper(void) {
  int i, n;
  char b[512];
  while ((n = read(0, b, 512)) > 0) {
    for (i = 0; i < n; ++i) {
      b[i] = toupper(b[i]);
    }
    write(1, b, n);
  }
  return 0;
}

static int Usleep(void) {
  int f;
  struct timespec t;
  if (n > 1) {
    f = 0;
    t = timespec_frommicros(atoi(args[1]));
  } else {
    f = TIMER_ABSTIME;
    t = timespec_max;
  }
  return clock_nanosleep(0, f, &t, 0);
}

static int Test(void) {
  int w, m = n;
  struct stat st;
  if (m && READ16LE(args[m - 1]) == READ16LE("]"))
    --m;
  if (m == 4) {
    w = READ32LE(args[2]) & 0x00ffffff;
    if ((w & 65535) == READ16LE("="))
      return !!strcmp(args[1], args[3]);
    if (w == READ24("=="))
      return !!strcmp(args[1], args[3]);
    if (w == READ24("!="))
      return !strcmp(args[1], args[3]);
  } else if (m == 3) {
    w = READ32LE(args[1]) & 0x00ffffff;
    if (w == READ24("-n"))
      return !(strlen(args[2]) > 0);
    if (w == READ24("-z"))
      return !(strlen(args[2]) == 0);
    if (w == READ24("-e"))
      return !!stat(args[2], &st);
    if (w == READ24("-f"))
      return !(!stat(args[2], &st) && S_ISREG(st.st_mode));
    if (w == READ24("-d"))
      return !(!stat(args[2], &st) && S_ISDIR(st.st_mode));
    if (w == READ24("-h"))
      return !(!stat(args[2], &st) && S_ISLNK(st.st_mode));
  }
  return -1;  // fall back to system test command
}

static int Rm(void) {
  int i, j;
  bool force = false;
  for (i = 1; i < n; ++i) {
    if (args[i][0] == '-' && args[i][1]) {
      if (args[i][1] == '-' && !args[i][2]) {
        ++i;  // rm -- terminates option parsing
        break;
      }
      for (j = 1; j < args[i][j]; ++j) {
        if (args[i][j] == 'f') {
          force = true;  // rm -f forces removal
          continue;
        }
        char option[2] = {args[i][j]};
        tinyprint(2, "rm", ": illegal option -- ", option, "\n", NULL);
        return 1;
      }
    } else {
      break;
    }
  }
  if (i == n) {
    return NeedArgument("rm");
  }
  for (; i < n; ++i) {
    struct stat st;
    if ((!force && (lstat(args[i], &st) ||
                    (!S_ISLNK(st.st_mode) && access(args[i], W_OK)))) ||
        unlink(args[i])) {
      if (force && errno == ENOENT)
        continue;
      perror(args[i]);
      return 1;
    }
  }
  return 0;
}

static int Rmdir(void) {
  int i;
  for (i = 1; i < n; ++i) {
    if (rmdir(args[i])) {
      perror(args[i]);
      return 1;
    }
  }
  return 0;
}

static int Touch(void) {
  int i;
  if (n > 1 && args[1][0] != '-') {
    for (i = 1; i < n; ++i) {
      if (touch(args[i], 0644)) {
        perror(args[i]);
        return 1;
      }
    }
    return 0;
  } else {
    return -1;  // fall back to system rmdir command
  }
}

static int Shift(int i) {
  if (i <= n) {
    memmove(args, args + i, (n - i + 1) * sizeof(*args));
    n -= i;
  }
  return 0;
}

static int Fake(int main(int, char **), bool wantexec) {
  int pid;
  if (wantexec) {
    goto RunProgram;
  }
  if ((pid = fork()) == -1) {
    perror("fork");
    return 127;
  }
  if (!pid) {
  RunProgram:
    // TODO(jart): Maybe nuke stdio too?
    if (_weaken(optind)) {
      *_weaken(optind) = 1;
    }
    environ = envs;
    exit(main(n, args));
  }
  return Waiter(pid);
}

static int Env(void) {
  int i, j;
  const char *arg;
  char term = '\n';
  for (i = 1; i < n; ++i) {
    if (args[i][0] == '-') {
      if (!args[i][1]) {
        envs[0] = 0;  // env - clears environment
        continue;
      }
      for (j = 1; j < args[i][j]; ++j) {
        if (args[i][j] == 'i') {
          envs[0] = 0;  // env -i clears environment
          continue;
        }
        if (args[i][j] == '0') {
          term = 0;  // env -0 uses '\0' line separator
          continue;
        }
        if ((arg = GetOptArg('u', &i, j))) {
          UnsetEnv(envs, arg);  // env -u VAR removes variable
          break;
        }
        char option[2] = {args[i][j]};
        tinyprint(2, "env", ": illegal option -- ", option, "\n", NULL);
        return 1;
      }
      continue;
    }
    if (strchr(args[i], '=')) {
      PutEnv(envs, args[i]);
    } else {
      Shift(i);
      return ShellSpawn();
    }
  }
  for (i = 0; envs[i]; ++i) {
    Write(1, envs[i]);
    write(1, &term, 1);
  }
  return 0;
}

static wontreturn void Exec(void) {
  Shift(1);
  if (!ShellExec()) {
    _Exit(0);  // can happen for builtins
  } else {
    perror("exec");
    _Exit(127);  // sh exec never returns
  }
}

static int TryBuiltin(bool wantexec) {
  if (!n)
    return exitstatus;
  if (!strcmp(args[0], "exit"))
    Exit();
  if (!strcmp(args[0], "exec"))
    Exec();
  if (!strcmp(args[0], "cd"))
    return Cd();
  if (!strcmp(args[0], "rm"))
    return Rm();
  if (!strcmp(args[0], "["))
    return Test();
  if (!strcmp(args[0], "cat"))
    return Cat();
  if (!strcmp(args[0], "env"))
    return Env();
  if (!strcmp(args[0], "pwd"))
    return Pwd();
  if (!strcmp(args[0], "wait"))
    return Wait();
  if (!strcmp(args[0], "echo"))
    return Echo();
  if (!strcmp(args[0], "read"))
    return Read();
  if (!strcmp(args[0], "true"))
    return True();
  if (!strcmp(args[0], "test"))
    return Test();
  if (!strcmp(args[0], "kill"))
    return Kill();
  if (!strcmp(args[0], "pause"))
    return Pause();
  if (!strcmp(args[0], "flock"))
    return Flock();
  if (!strcmp(args[0], "chmod"))
    return Chmod();
  if (!strcmp(args[0], "touch"))
    return Touch();
  if (!strcmp(args[0], "rmdir"))
    return Rmdir();
  if (!strcmp(args[0], "mkdir"))
    return Mkdir();
  if (!strcmp(args[0], "false"))
    return False();
  if (!strcmp(args[0], "mktemp"))
    return Mktemp();
  if (!strcmp(args[0], "usleep"))
    return Usleep();
  if (!strcmp(args[0], "toupper"))
    return Toupper();
  if (!strcmp(args[0], "tr"))
    return Fake(_tr, wantexec);
  if (!strcmp(args[0], "sed"))
    return Fake(_sed, wantexec);
  if (_weaken(_awk) && strcmp(args[0], "awk"))
    return Fake(_weaken(_awk), wantexec);
  if (_weaken(_curl) && !strcmp(args[0], "curl")) {
    return Fake(_weaken(_curl), wantexec);
  }
  return -1;
}

static int ShellExec(void) {
  int rc;
  if ((rc = TryBuiltin(true)) == -1) {
    rc = SystemExec();
  }
  return (n = 0), rc;
}

static void Pipe(void) {
  int pid, pfds[2];
  if (pipe2(pfds, O_CLOEXEC)) {
    perror("pipe");
    _Exit(127);
  }
  if ((pid = fork()) == -1) {
    perror("fork");
    _Exit(127);
  }
  if (!pid) {
    unassert(dup2(pfds[1], 1) == 1);
    // we can't rely on cloexec because builtins
    if (pfds[0] != 1)
      unassert(!close(pfds[0]));
    if (pfds[1] != 1)
      unassert(!close(pfds[1]));
    _Exit(ShellExec());
  }
  unassert(dup2(pfds[0], 0) == 0);
  if (pfds[0] != 0)
    unassert(!close(pfds[0]));
  if (pfds[1] != 0)
    unassert(!close(pfds[1]));
  n = 0;
}

static int ShellSpawn(void) {
  int rc, pid;
  if ((rc = TryBuiltin(false)) == -1) {
    switch ((pid = fork())) {
      case 0:
        _Exit(SystemExec());
      default:
        rc = Waiter(pid);
        break;
      case -1:
        perror("fork");
        rc = 127;
        break;
    }
  }
  return (n = 0), rc;
}

static void ShellSpawnAsync(void) {
  switch ((lastchild = fork())) {
    case 0:
      _Exit(ShellExec());
    default:
      break;
    case -1:
      perror("fork");
      break;
  }
  n = 0;
}

static const char *IntToStr(int x) {
  static char ibuf[12];
  FormatInt32(ibuf, x);
  return ibuf;
}

static const char *GetVar(const char *key) {
  static char vbuf[PATH_MAX];
  if (key[0] == '$' && !key[1]) {
    return IntToStr(getpid());
  } else if (key[0] == '!' && !key[1]) {
    return IntToStr(lastchild);
  } else if (key[0] == '?' && !key[1]) {
    return IntToStr(exitstatus);
  } else if (!strcmp(key, "PWD")) {
    npassert(__getcwd(vbuf, sizeof(vbuf)) != -1);
    return vbuf;
  } else if (!strcmp(key, "UID")) {
    FormatInt32(vbuf, getuid());
    return vbuf;
  } else if (!strcmp(key, "GID")) {
    FormatInt32(vbuf, getgid());
    return vbuf;
  } else if (!strcmp(key, "EUID")) {
    FormatInt32(vbuf, geteuid());
    return vbuf;
  } else {
    return __getenv(envs, key).s;
  }
}

static bool IsVarName(int c) {
  return isalnum(c) || c == '_' ||
         (!vari && (c == '?' || c == '!' || c == '$'));
}

static bool CopyVar(void) {
  size_t j;
  const char *s;
  if (IsVarName(*p)) {
    npassert(vari + 1 < sizeof(var));
    var[vari++] = *p;
    var[vari] = 0;
    return false;
  }
  if ((s = GetVar(var))) {
    if ((j = strlen(s))) {
      npassert(q + j < argbuf + sizeof(argbuf));
      q = mempcpy(q, s, j);
    }
  }
  return true;
}

static char *Tokenize(void) {
  int c, t, rc;
  for (t = STATE_WHITESPACE;; ++p) {
    switch (t) {

      case STATE_WHITESPACE:
        if (!*p)
          return 0;
        if (*p == ' ' || *p == '\t' || *p == '\n' ||
            (p[0] == '\\' && p[1] == '\n')) {
          continue;
        }
        t = STATE_CMD;
        // fallthrough

      case STATE_CMD:
        if (unsupported[*p & 255]) {
          UnsupportedSyntax(*p);
        }
        if (!*p || *p == ' ' || *p == '\t') {
          return Finish();
        } else if (*p == '"') {
          t = STATE_QUOTED;
        } else if (*p == '\'') {
          t = STATE_SINGLE;
        } else if (*p == '$') {
          t = STATE_VAR;
          var[(vari = 0)] = 0;
        } else if (*p == '\\') {
          if (!p[1])
            UnsupportedSyntax(*p);
          Append(*++p);
        } else if (*p == '=') {
          if (!n && q > r)
            assign = r;
          Append(*p);
        } else if (*p == '|') {
          if (q > r) {
            return Finish();
          } else if (p[1] == '|') {
            rc = ShellSpawn();
            if (!rc) {
              _Exit(0);
            } else {
              ++p;
              t = STATE_WHITESPACE;
            }
          } else {
            Pipe();
            t = STATE_WHITESPACE;
          }
        } else if (*p == ';' || *p == '\n') {
          if (q > r) {
            return Finish();
          } else {
            exitstatus = ShellSpawn();
            t = STATE_WHITESPACE;
          }
        } else if (*p == '>') {
          Append(*p);
          if (p[1] == '&') {
            Append(*++p);
          }
        } else if (*p == '&') {
          if (q > r) {
            return Finish();
          } else if (p[1] == '&') {
            rc = ShellSpawn();
            if (!rc) {
              ++p;
              t = STATE_WHITESPACE;
            } else {
              _Exit(rc);
            }
          } else {
            ShellSpawnAsync();
            t = STATE_WHITESPACE;
          }
        } else {
          Append(*p);
        }
        break;

      case STATE_VAR:
        // XXX: we need to find a simple elegant way to break up
        //      unquoted variable expansions into multiple args.
        if (CopyVar())
          t = STATE_CMD, --p;
        break;

      case STATE_SINGLE:
        if (!*p)
          goto UnterminatedString;
        if (*p == '\'') {
          t = STATE_CMD;
        } else {
          *q++ = *p;
        }
        break;

      UnterminatedString:
        tinyprint(2, "cmd: error: unterminated string\n", NULL);
        _Exit(6);

      case STATE_QUOTED_VAR:
        if (!*p)
          goto UnterminatedString;
        if (CopyVar())
          t = STATE_QUOTED, --p;
        break;

      case STATE_QUOTED:
        if (!*p)
          goto UnterminatedString;
        if (*p == '"') {
          t = STATE_CMD;
        } else if (p[0] == '$') {
          t = STATE_QUOTED_VAR;
          var[(vari = 0)] = 0;
        } else if (p[0] == '\\') {
          switch ((c = *++p)) {
            case 0:
              UnsupportedSyntax('\\');
            case '\n':
              break;
            case '$':
            case '`':
            case '"':
              *q++ = c;
              break;
            default:
              *q++ = '\\';
              *q++ = c;
              break;
          }
        } else {
          *q++ = *p;
        }
        break;

      default:
        __builtin_unreachable();
    }
  }
}

static const char *GetRedirectArg(const char *prog, const char *arg, int n) {
  if (arg[n]) {
    return arg + n;
  } else if ((arg = Tokenize())) {
    return arg;
  } else {
    tinyprint(2, prog, ": error: redirect missing path\n", NULL);
    _Exit(14);
  }
}

int _cocmd(int argc, char **argv, char **envp) {
  size_t i;
  char *arg;
  size_t globCount = 0;
  int globFlags = 0;
  glob_t globTheBuilder;
  prog = argc > 0 ? argv[0] : "cocmd";

  for (i = 1; i < 32; ++i) {
    unsupported[i] = true;
  }
  unsupported['\t'] = false;
  unsupported['\n'] = false;
  unsupported[0177] = true;
  unsupported['~'] = true;
  unsupported['`'] = true;
  unsupported['#'] = true;
  unsupported['('] = true;
  unsupported[')'] = true;
  unsupported['{'] = true;
  // Perl t/op/exec.t depends on unpaired } being
  // passed from the shell to Perl
  unsupported['}'] = false;

  if (argc >= 3 && !strcmp(argv[1], "--")) {
    for (i = 2; i < argc; ++i) {
      args[n++] = argv[i];
    }
    _Exit(ShellExec());
  }

  if (argc != 3) {
    tinyprint(2, prog, ": error: wrong number of args\n", NULL);
    _Exit(10);
  }

  if (strcmp(argv[1], "-c")) {
    tinyprint(2, prog, ": error: argv[1] should -c\n", NULL);
    _Exit(11);
  }

  p = cmd = argv[2];
  if (strlen(cmd) >= ARG_MAX) {
    tinyprint(2, prog, ": error: cmd too long: ", cmd, "\n", NULL);
    _Exit(12);
  }

  // copy environment variables
  int envi = 0;
  if (envp) {
    for (; envp[envi]; ++envi) {
      npassert(envi + 1 < ARRAYLEN(envs));
      envs[envi] = envp[envi];
    }
  }
  envs[envi] = 0;

  // get command line arguments
  n = 0;
  r = q = argbuf;
  while ((arg = Tokenize())) {
    if (arg == TOMBSTONE)
      continue;
    if (n + 1 < ARRAYLEN(args)) {
      if (isdigit(arg[0]) && arg[1] == '>' && arg[2] == '&' &&
          isdigit(arg[3])) {
        dup2(arg[3] - '0', arg[0] - '0');
      } else if (arg[0] == '>' && arg[1] == '&' && isdigit(arg[2])) {
        dup2(arg[2] - '0', 1);
      } else if (isdigit(arg[0]) && arg[1] == '>' && arg[2] == '>') {
        Open(GetRedirectArg(prog, arg, 3), arg[0] - '0',
             O_WRONLY | O_CREAT | O_APPEND);
      } else if (arg[0] == '>' && arg[1] == '>') {
        Open(GetRedirectArg(prog, arg, 2), 1, O_WRONLY | O_CREAT | O_APPEND);
      } else if (isdigit(arg[0]) && arg[1] == '>') {
        Open(GetRedirectArg(prog, arg, 2), arg[0] - '0',
             O_WRONLY | O_CREAT | O_TRUNC);
      } else if (arg[0] == '>') {
        Open(GetRedirectArg(prog, arg, 1), 1, O_WRONLY | O_CREAT | O_TRUNC);
      } else if (arg[0] == '<') {
        Open(GetRedirectArg(prog, arg, 1), 0, O_RDONLY);
      } else {
        int globrc = GLOB_NOMATCH;
        globrc = glob(arg, globFlags, NULL, &globTheBuilder);
        if (globrc == 0) {
          for (; globCount < globTheBuilder.gl_pathc; globCount++) {
            args[n++] = globTheBuilder.gl_pathv[globCount];
          }
        } else if (globrc != GLOB_NOMATCH) {
          tinyprint(2, prog, ": error: with glob\n", NULL);
          _Exit(16);
        }
        globFlags |= GLOB_APPEND;
        if (globrc == GLOB_NOMATCH) {
          args[n++] = arg;
        }
        args[n] = 0;
      }
    } else {
      tinyprint(2, prog, ": error: too many args\n", NULL);
      _Exit(13);
    }
  }

  return ShellExec();
}
