diff --git a/Makefile b/Makefile
index d567b1f..7fda874 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,7 @@ SRC = \
 	chvt.c     \
 	cksum.c    \
 	cmp.c      \
+	comm.c     \
 	cp.c       \
 	date.c     \
 	dirname.c  \
diff --git a/comm.c b/comm.c
new file mode 100644
index 0000000..75cbff2
--- /dev/null
+++ b/comm.c
@@ -0,0 +1,90 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <limits.h>
+#include <string.h>
+#include "util.h"
+
+#define CLAMP(x, l, h) MIN(h, MAX(l, x))
+
+static void printline(int, char*);
+static char *nextline(char*, int, FILE*, char*);
+static void finish(int, FILE*, char*);
+
+static int show[] = {1, 1, 1};
+
+static void
+usage(void)
+{
+	eprintf("usage: comm [-123] file1 file2\n");
+}
+
+int
+main(int argc, char **argv)
+{
+	int x, dif = 0;
+	char lines[2][LINE_MAX+1];
+	FILE *files[2];
+
+	ARGBEGIN {
+	case '1':
+	case '2':
+	case '3':
+		show[ARGC()-'1'] = 0;
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	if(argc != 2)
+		usage();
+
+	for(x = 0; x < LEN(files); x++)
+		if(!(files[x] = fopen(argv[x], "r")))
+			eprintf("comm: '%s':", argv[x]);
+
+	for(;;){
+		if(dif <= 0)
+			if(!nextline(lines[0], sizeof *lines, files[0], argv[0]))
+				finish(1, files[1], argv[1]);
+		if(dif >= 0)
+			if(!nextline(lines[1], sizeof *lines, files[1], argv[1]))
+				finish(0, files[0], argv[0]);
+		dif = strcmp(lines[0], lines[1]);
+		dif = CLAMP(dif, -1, 1);
+		printline((2-dif)%3,  lines[MAX(0, dif)]);
+	}
+	return 0;
+}
+
+void
+printline(int pos, char *line)
+{
+	int x;
+	if(!show[pos])
+		return;
+	for(x=0; x<pos; x++)
+		if(show[x])
+			putchar('\t');
+	printf("%s", line);
+}
+
+char *
+nextline(char *buf, int n, FILE *f, char *name)
+{
+	buf  = fgets(buf, n, f);
+	if(!buf && !feof(f))
+		eprintf("comm: '%s':", name);
+	if(buf && !strchr(buf, '\n'))
+		eprintf("comm: '%s': line too long\n", name);
+	return buf;
+}
+
+void
+finish(int pos,  FILE *f, char *name)
+{
+	char buf[LINE_MAX+1];
+	while(nextline(buf, sizeof buf, f, name))
+		printline(pos, buf);
+	exit(EXIT_SUCCESS);
+}
+
