diff --git a/Makefile b/Makefile
index d90d690..05c12dd 100644
--- a/Makefile
+++ b/Makefile
@@ -25,6 +25,7 @@ SRC = \
 	chgrp.c    \
 	chmod.c    \
 	chown.c    \
+	chroot.c   \
 	chvt.c     \
 	cksum.c    \
 	cmp.c      \
@@ -32,6 +33,7 @@ SRC = \
 	date.c     \
 	dirname.c  \
 	echo.c     \
+	env.c      \
 	false.c    \
 	fold.c     \
 	grep.c     \
@@ -53,6 +55,7 @@ SRC = \
 	rmdir.c    \
 	sleep.c    \
 	sort.c     \
+	split.c    \
 	sync.c     \
 	tail.c     \
 	tee.c      \
@@ -65,6 +68,7 @@ SRC = \
 	unlink.c   \
 	seq.c      \
 	wc.c       \
+	who.c      \
 	yes.c
 
 OBJ = $(SRC:.c=.o) $(LIB)
diff --git a/TODO b/TODO
index 217829f..00d3839 100644
--- a/TODO
+++ b/TODO
@@ -10,8 +10,6 @@ diff [-ru] file1 file2
 
 du [-hdi] [path]
 
-env [-u] [name=value...] [command]
-
 expand [-i] [-t N] [file...]
 
 expr [expression]
@@ -32,8 +30,6 @@ seq [-s string] [N [N]] N
 
 sha1sum [-c] [file...]
 
-split [-a N] [-b N] [-l N] [input [prefix]]
-
 test [expression...]
 
 tr string1 [string2]
diff --git a/chroot.1 b/chroot.1
new file mode 100644
index 0000000..48cbe19
--- /dev/null
+++ b/chroot.1
@@ -0,0 +1,26 @@
+.TH CHROOT 1 sbase\-VERSION
+.SH NAME
+chroot \- invoke a command with a different root directory
+.SH SYNOPSIS
+.B chroot
+.IR dir
+.RI [ command
+.RI [ arg ...]]
+
+.SH DESCRIPTION
+.B chroot
+runs 
+.IR command
+after changing the root directory to 
+.IR dir
+with the
+.B chroot
+system call, and changing the working directory to the new root.
+
+If 
+.IR command
+is not specified, an interactive shell is started in the new root.
+
+.SH SEE ALSO
+.IR chroot (2)
+.IR chdir (2)
diff --git a/chroot.c b/chroot.c
new file mode 100644
index 0000000..3abab0f
--- /dev/null
+++ b/chroot.c
@@ -0,0 +1,38 @@
+#include <stdlib.h>
+#include <unistd.h>
+#include "util.h"
+
+static void usage(void);
+
+int
+main(int argc, char **argv)
+{
+	char *shell[] = {"/bin/sh", "-i", NULL};
+	
+	if(getenv("SHELL"))
+		shell[0] = getenv("SHELL");
+
+	if(argc < 2)
+		usage();
+	
+	if(chroot(argv[1]) == -1)
+		eprintf("chroot: '%s':", argv[1]);
+	
+	if(chdir("/") == -1)
+		eprintf("chroot:");
+
+	if(argc == 2) {
+		execvp(*shell, shell);
+	} else {
+		execvp(argv[2], argv+2);
+	}
+	
+	eprintf("chroot: '%s':", argv[2]);
+	return 1;
+}
+
+void
+usage(void)
+{
+	eprintf("usage: chroot dir [command [arg...]]\n");
+}
diff --git a/env.1 b/env.1
new file mode 100644
index 0000000..b77fb2f
--- /dev/null
+++ b/env.1
@@ -0,0 +1,41 @@
+.TH ENV 1 sbase\-VERSION
+.SH NAME
+env \- modify the environment, then print it or run a command.
+.SH SYNOPSIS
+.B env
+.RB [ \-i ]
+.RB [ \-u 
+.IR name ]...
+.RI [ name=value ]...
+.RI [ cmd 
+.RI [ arg ...]]
+
+.SH DESCRIPTION
+.B env
+removes part of the environment according to the flags, then adds or 
+sets each variable specified by
+.IR name 
+to equal
+.IR value .
+
+If 
+.IR cmd
+is given, it is executed in this new environment; otherwise, the 
+modified environment is printed to standard out.
+
+.SH OPTIONS
+.TP
+.B \-i
+Comptetely ignore the existing environment; start fresh.
+
+.TP
+.B \-u name
+Unsets
+.IR name
+from the environment.
+
+.SH SEE ALSO
+.IR printenv (1)
+.IR putenv (3)
+.IR environ (7)
+
diff --git a/env.c b/env.c
new file mode 100644
index 0000000..718b13e
--- /dev/null
+++ b/env.c
@@ -0,0 +1,44 @@
+#include <unistd.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "util.h"
+
+extern char **environ;
+
+static void usage(void);
+
+int
+main(int argc, char **argv)
+{
+	ARGBEGIN {
+	case 'i':
+		clearenv();
+		break;
+	case 'u':
+		unsetenv(EARGF(usage()));
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	for(; *argv && strchr(*argv, '='); argv++)
+		putenv(*argv);
+
+	if(*argv) {
+		execvp(*argv, argv);
+		enprintf(127-(errno!=EEXIST), "env: '%s':", *argv);
+	}
+
+	while(environ && *environ)
+		printf("%s\n", *environ++);	
+
+	return 0;
+}
+
+void
+usage(void)
+{
+	eprintf("usage: env [-i] [-u name]... [name=value]... [cmd [arg...]]\n");
+}
diff --git a/split.1 b/split.1
new file mode 100644
index 0000000..e615168
--- /dev/null
+++ b/split.1
@@ -0,0 +1,58 @@
+.TH SPLIT 1 sbase\-VERSION
+.SH NAME
+split \- split up a file
+.SH SYNOPSIS
+.B split
+.RB [ \-d ]
+.RB [ \-a 
+.IR len ]
+.RB [ \-b 
+.RI [ bytes [k|m|g]]]
+.RB [ \-l 
+.RI [ lines ]]
+.RI [ input 
+.RI [ prefix ]]
+
+.SH DESCRIPTION
+.B split
+Reads a file, splitting it into smaller files, every 
+.IR bytes
+bytes
+or
+.IR lines
+lines. If
+.B split
+runs out of filenames before all the data can be written, it stops at the
+last valid filename, leaving all the written data on the disk. 
+
+The 
+.IR b
+and
+.IR l
+flags are mutually exclusive. Only the last one specified will be obeyed.
+
+.SH OPTIONS
+.TP
+.B \-d
+Use decimal suffixes rather than alphabetical.
+
+.TP
+.B \-a "len"
+Set the suffix length to 
+.IR len
+characters long.
+
+.TP
+.B \-b [bytes[k|m|g]]
+Start a new file every 
+.IR bytes
+bytes. The units k, m, and g are case insensitive, and powers of 2, not 10.
+
+.TP
+.B \-l [lines]
+Start a new file every 
+.IR lines
+lines.
+
+.SH SEE ALSO
+.IR cat (1)
diff --git a/split.c b/split.c
new file mode 100644
index 0000000..87e1099
--- /dev/null
+++ b/split.c
@@ -0,0 +1,138 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h>
+#include <ctype.h>
+#include <limits.h>
+#include "util.h"
+
+static int itostr(char *, int, int);
+static FILE *nextfile(FILE *, char *, int, int);
+static void usage(void);
+
+static int base = 26, start = 'a';
+
+int 
+main(int argc, char **argv)
+{
+	int plen, slen = 2;
+	int ch;
+	char name[NAME_MAX+1];
+	char *prefix = "x";
+	char *file = NULL;
+	char *tmp, *end;
+	uint64_t sizes['M'+1];
+	uint64_t size = 1000, scale, n;
+	int always = 0;
+	FILE *in=stdin, *out=NULL;
+
+       	sizes['K'] = 1024;
+	sizes['M'] = 1024L*1024L;
+	sizes['G'] = 1024L*1024L*1024L;
+
+	ARGBEGIN {
+	case 'b':
+		always = 1;
+		tmp = ARGF();
+		if(tmp == NULL)
+			break;
+		
+		size = strtoull(tmp, &end, 10);
+		if(*end == '\0')
+			break;
+		if(strchr("KMG", toupper(*end)) == NULL || end[1] != '\0')
+			usage();
+		scale = sizes[toupper(*end)];
+		if(size > (UINT64_MAX/scale))
+			eprintf("split: '%s': out of range\n", tmp);
+		size *= scale;
+		break;
+	case 'l':
+		always = 0;
+		tmp = ARGF();
+		if(tmp)
+			size = estrtol(tmp, 10);
+		break;
+	case 'a':
+		slen = estrtol(EARGF(usage()), 10);
+		break;
+	case 'd':
+		base = 10;
+		start = '0';
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	if(*argv)
+		file = *argv++;
+	if(*argv)
+		prefix = *argv++;
+	if(*argv)
+		usage();
+
+	plen = strlen(prefix);
+	if(plen+slen > NAME_MAX)
+		eprintf("split: names cannot exceed %d bytes", NAME_MAX);
+	strcpy(name, prefix);
+
+	if(file && strcmp(file, "-") != 0) {
+		in = fopen(file, "r");
+		if(!in)
+			eprintf("split: '%s':", file);
+	}
+
+Nextfile:
+	while((out = nextfile(out, name, plen, slen))) {
+		n = 0;
+		while((ch = getc(in)) != EOF) {
+			putc(ch, out);
+			n += (always || ch == '\n');
+			if(n >= size)
+				goto Nextfile;
+		}
+		fclose(out);
+		break;
+	}
+	return 0;
+
+}
+
+void
+usage(void)
+{
+	eprintf("usage: split [-d] [-a len] [-b [bytes[k|m|g]]] [-l [lines]] [input [prefix]]\n");
+}
+
+
+int
+itostr(char *str, int x, int n)
+{
+	str[n] = '\0';
+	while(n-- > 0) {
+		str[n] = start + (x % base);
+		x /= base;
+	}
+	if(x)
+		return -1;
+	return 0;
+}
+
+FILE *
+nextfile(FILE *f, char *buf, int plen, int slen)
+{
+	static int n = 0;
+	int s;
+
+	if(f)
+		fclose(f);
+	s = itostr(buf+plen, n++, slen);
+	if(s == -1)
+		return NULL;
+
+	f = fopen(buf, "w");
+	if(!f)
+		eprintf("split: '%s':", buf);
+	return f;
+}
+
diff --git a/who.1 b/who.1
new file mode 100644
index 0000000..68ca7f1
--- /dev/null
+++ b/who.1
@@ -0,0 +1,24 @@
+.TH WHO 1 sbase\-VERSION
+.SH NAME
+who \- print who has logged on
+.SH SYNOPSIS
+.B who
+
+.SH DESCRIPTION
+.B who
+prints a list of who has logged on, their controlling tty, and the 
+time at which they logged on.
+
+.SH BUGS
+.B who
+relies on the utmp file to be updated responsibly. This
+doesn't always happen, which can cause who to print completely
+bogus data.
+
+musl\-libc currently implements all utmpx functions as stubs Obviously,
+this command cannot work under such conditions.
+
+.SH SEE ALSO
+.IR getutxent (3)
+.IR utmpx (5)
+
diff --git a/who.c b/who.c
new file mode 100644
index 0000000..08c3c2b
--- /dev/null
+++ b/who.c
@@ -0,0 +1,37 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <utmpx.h>
+#include "util.h"
+
+static void usage(void);
+
+int
+main(int argc, char **argv)
+{
+	struct utmpx *ut;
+	time_t t;
+	char timebuf[sizeof "yyyy-mm-dd hh:mm"];
+
+	if(argc!=1)
+		usage();
+
+	while((ut=getutxent())) {
+		if(ut->ut_type != USER_PROCESS)
+			continue;
+		t = ut->ut_tv.tv_sec;
+		strftime(timebuf, sizeof timebuf, "%Y-%m-%d %H:%M", localtime(&t));
+		printf("%-8s %-12s %-16s\n", ut->ut_user, ut->ut_line, timebuf);
+	}
+	endutxent();
+	return 0;
+}
+
+void 
+usage(void)
+{
+	eprintf("usage: who\n");
+}
+
