diff --git a/Cargo.toml b/Cargo.toml
index 3bf5477e..29586bc1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,6 +18,10 @@ bench = false
 path = "src/main.rs"
 name = "rg"
 
+[[test]]
+name = "integration"
+path = "tests/tests.rs"
+
 [dependencies]
 crossbeam = "0.2"
 docopt = "0.6"
diff --git a/src/args.rs b/src/args.rs
index d6a9657f..ea7299fe 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -109,10 +109,6 @@ Less common options:
     -L, --follow
         Follow symlinks.
 
-    --line-terminator ARG
-        The byte to use for a line terminator. Escape sequences may be used.
-        [default: \\n]
-
     --mmap
         Search using memory maps when possible. This is enabled by default
         when ripgrep thinks it will be faster. (Note that mmap searching
@@ -174,7 +170,6 @@ pub struct RawArgs {
     flag_ignore_case: bool,
     flag_invert_match: bool,
     flag_line_number: bool,
-    flag_line_terminator: String,
     flag_literal: bool,
     flag_mmap: bool,
     flag_no_heading: bool,
@@ -248,7 +243,9 @@ impl RawArgs {
         };
         let paths =
             if self.arg_path.is_empty() {
-                if sys::stdin_is_atty() {
+                if sys::stdin_is_atty()
+                    || self.flag_files
+                    || self.flag_type_list {
                     vec![Path::new("./").to_path_buf()]
                 } else {
                     vec![Path::new("-").to_path_buf()]
@@ -277,15 +274,6 @@ impl RawArgs {
         if mmap {
             debug!("will try to use memory maps");
         }
-        let eol = {
-            let eol = unescape(&self.flag_line_terminator);
-            if eol.is_empty() {
-                errored!("Empty line terminator is not allowed.");
-            } else if eol.len() > 1 {
-                errored!("Line terminators are limited to exactly 1 byte.");
-            }
-            eol[0]
-        };
         let glob_overrides =
             if self.flag_glob.is_empty() {
                 None
@@ -309,6 +297,7 @@ impl RawArgs {
             } else {
                 self.flag_color == "always"
             };
+        let eol = b'\n';
         let mut with_filename = self.flag_with_filename;
         if !with_filename {
             with_filename = paths.len() > 1 || paths[0].is_dir();
diff --git a/src/search.rs b/src/search.rs
index 523d1e4d..027bd0d3 100644
--- a/src/search.rs
+++ b/src/search.rs
@@ -695,8 +695,7 @@ mod tests {
 
     use super::{InputBuffer, Searcher, start_of_previous_lines};
 
-    lazy_static! {
-        static ref SHERLOCK: &'static str = "\
+    const SHERLOCK: &'static str = "\
 For the Doctor Watsons of this world, as opposed to the Sherlock
 Holmeses, success in the province of detective work must always
 be, to a very large extent, the result of luck. Sherlock Holmes
@@ -704,7 +703,8 @@ can extract a clew from a wisp of straw or a flake of cigar ash;
 but Doctor Watson has to have it taken out for him and dusted,
 and exhibited clearly, with a label attached.\
 ";
-        static ref CODE: &'static str = "\
+
+    const CODE: &'static str = "\
 extern crate snap;
 
 use std::io;
@@ -719,7 +719,6 @@ fn main() {
     io::copy(&mut rdr, &mut wtr).expect(\"I/O operation failed\");
 }
 ";
-    }
 
     fn hay(s: &str) -> io::Cursor<Vec<u8>> {
         io::Cursor::new(s.to_string().into_bytes())
@@ -874,7 +873,7 @@ fn main() {
 
     #[test]
     fn basic_search1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s|s);
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s|s);
         assert_eq!(2, count);
         assert_eq!(out, "\
 /baz.rs:For the Doctor Watsons of this world, as opposed to the Sherlock
@@ -901,7 +900,7 @@ fn main() {
     #[test]
     fn line_numbers() {
         let (count, out) = search_smallcap(
-            "Sherlock", &*SHERLOCK, |s| s.line_number(true));
+            "Sherlock", SHERLOCK, |s| s.line_number(true));
         assert_eq!(2, count);
         assert_eq!(out, "\
 /baz.rs:1:For the Doctor Watsons of this world, as opposed to the Sherlock
@@ -912,7 +911,7 @@ fn main() {
     #[test]
     fn count() {
         let (count, out) = search_smallcap(
-            "Sherlock", &*SHERLOCK, |s| s.count(true));
+            "Sherlock", SHERLOCK, |s| s.count(true));
         assert_eq!(2, count);
         assert_eq!(out, "/baz.rs:2\n");
     }
@@ -920,7 +919,7 @@ fn main() {
     #[test]
     fn invert_match() {
         let (count, out) = search_smallcap(
-            "Sherlock", &*SHERLOCK, |s| s.invert_match(true));
+            "Sherlock", SHERLOCK, |s| s.invert_match(true));
         assert_eq!(4, count);
         assert_eq!(out, "\
 /baz.rs:Holmeses, success in the province of detective work must always
@@ -932,7 +931,7 @@ fn main() {
 
     #[test]
     fn invert_match_line_numbers() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.invert_match(true).line_number(true)
         });
         assert_eq!(4, count);
@@ -946,7 +945,7 @@ fn main() {
 
     #[test]
     fn invert_match_count() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.invert_match(true).count(true)
         });
         assert_eq!(4, count);
@@ -955,7 +954,7 @@ fn main() {
 
     #[test]
     fn before_context_one1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.line_number(true).before_context(1)
         });
         assert_eq!(2, count);
@@ -968,7 +967,7 @@ fn main() {
 
     #[test]
     fn before_context_invert_one1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.line_number(true).before_context(1).invert_match(true)
         });
         assert_eq!(4, count);
@@ -984,7 +983,7 @@ fn main() {
 
     #[test]
     fn before_context_invert_one2() {
-        let (count, out) = search_smallcap(" a ", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap(" a ", SHERLOCK, |s| {
             s.line_number(true).before_context(1).invert_match(true)
         });
         assert_eq!(3, count);
@@ -999,7 +998,7 @@ fn main() {
 
     #[test]
     fn before_context_two1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.line_number(true).before_context(2)
         });
         assert_eq!(2, count);
@@ -1012,7 +1011,7 @@ fn main() {
 
     #[test]
     fn before_context_two2() {
-        let (count, out) = search_smallcap("dusted", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("dusted", SHERLOCK, |s| {
             s.line_number(true).before_context(2)
         });
         assert_eq!(1, count);
@@ -1026,7 +1025,7 @@ fn main() {
     #[test]
     fn before_context_two3() {
         let (count, out) = search_smallcap(
-            "success|attached", &*SHERLOCK, |s| {
+            "success|attached", SHERLOCK, |s| {
                 s.line_number(true).before_context(2)
             });
         assert_eq!(2, count);
@@ -1042,7 +1041,7 @@ fn main() {
 
     #[test]
     fn before_context_two4() {
-        let (count, out) = search("stdin", &*CODE, |s| {
+        let (count, out) = search("stdin", CODE, |s| {
             s.line_number(true).before_context(2)
         });
         assert_eq!(3, count);
@@ -1059,7 +1058,7 @@ fn main() {
 
     #[test]
     fn before_context_two5() {
-        let (count, out) = search("stdout", &*CODE, |s| {
+        let (count, out) = search("stdout", CODE, |s| {
             s.line_number(true).before_context(2)
         });
         assert_eq!(2, count);
@@ -1076,7 +1075,7 @@ fn main() {
 
     #[test]
     fn before_context_three1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
                 s.line_number(true).before_context(3)
             });
         assert_eq!(2, count);
@@ -1089,7 +1088,7 @@ fn main() {
 
     #[test]
     fn after_context_one1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.line_number(true).after_context(1)
         });
         assert_eq!(2, count);
@@ -1103,7 +1102,7 @@ fn main() {
 
     #[test]
     fn after_context_invert_one1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.line_number(true).after_context(1).invert_match(true)
         });
         assert_eq!(4, count);
@@ -1118,7 +1117,7 @@ fn main() {
 
     #[test]
     fn after_context_invert_one2() {
-        let (count, out) = search_smallcap(" a ", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap(" a ", SHERLOCK, |s| {
             s.line_number(true).after_context(1).invert_match(true)
         });
         assert_eq!(3, count);
@@ -1134,7 +1133,7 @@ fn main() {
 
     #[test]
     fn after_context_two1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.line_number(true).after_context(2)
         });
         assert_eq!(2, count);
@@ -1149,7 +1148,7 @@ fn main() {
 
     #[test]
     fn after_context_two2() {
-        let (count, out) = search_smallcap("dusted", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("dusted", SHERLOCK, |s| {
             s.line_number(true).after_context(2)
         });
         assert_eq!(1, count);
@@ -1162,7 +1161,7 @@ fn main() {
     #[test]
     fn after_context_two3() {
         let (count, out) = search_smallcap(
-            "success|attached", &*SHERLOCK, |s| {
+            "success|attached", SHERLOCK, |s| {
                 s.line_number(true).after_context(2)
             });
         assert_eq!(2, count);
@@ -1177,7 +1176,7 @@ fn main() {
 
     #[test]
     fn after_context_three1() {
-        let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| {
             s.line_number(true).after_context(3)
         });
         assert_eq!(2, count);
@@ -1194,7 +1193,7 @@ fn main() {
     #[test]
     fn before_after_context_two1() {
         let (count, out) = search(
-            r"fn main|let mut rdr", &*CODE, |s| {
+            r"fn main|let mut rdr", CODE, |s| {
                 s.line_number(true).after_context(2).before_context(2)
             });
         assert_eq!(2, count);
diff --git a/src/search_buffer.rs b/src/search_buffer.rs
index 23578de9..fc8cd3a1 100644
--- a/src/search_buffer.rs
+++ b/src/search_buffer.rs
@@ -151,8 +151,7 @@ mod tests {
 
     use super::BufferSearcher;
 
-    lazy_static! {
-        static ref SHERLOCK: &'static str = "\
+    const SHERLOCK: &'static str = "\
 For the Doctor Watsons of this world, as opposed to the Sherlock
 Holmeses, success in the province of detective work must always
 be, to a very large extent, the result of luck. Sherlock Holmes
@@ -160,7 +159,8 @@ can extract a clew from a wisp of straw or a flake of cigar ash;
 but Doctor Watson has to have it taken out for him and dusted,
 and exhibited clearly, with a label attached.\
 ";
-        static ref CODE: &'static str = "\
+
+    const CODE: &'static str = "\
 extern crate snap;
 
 use std::io;
@@ -175,7 +175,6 @@ fn main() {
     io::copy(&mut rdr, &mut wtr).expect(\"I/O operation failed\");
 }
 ";
-    }
 
     fn matcher(pat: &str) -> Grep {
         GrepBuilder::new(pat).build().unwrap()
@@ -205,7 +204,7 @@ fn main() {
 
     #[test]
     fn basic_search() {
-        let (count, out) = search("Sherlock", &*SHERLOCK, |s|s);
+        let (count, out) = search("Sherlock", SHERLOCK, |s|s);
         assert_eq!(2, count);
         assert_eq!(out, "\
 /baz.rs:For the Doctor Watsons of this world, as opposed to the Sherlock
@@ -233,7 +232,7 @@ fn main() {
     #[test]
     fn line_numbers() {
         let (count, out) = search(
-            "Sherlock", &*SHERLOCK, |s| s.line_number(true));
+            "Sherlock", SHERLOCK, |s| s.line_number(true));
         assert_eq!(2, count);
         assert_eq!(out, "\
 /baz.rs:1:For the Doctor Watsons of this world, as opposed to the Sherlock
@@ -244,7 +243,7 @@ fn main() {
     #[test]
     fn count() {
         let (count, out) = search(
-            "Sherlock", &*SHERLOCK, |s| s.count(true));
+            "Sherlock", SHERLOCK, |s| s.count(true));
         assert_eq!(2, count);
         assert_eq!(out, "/baz.rs:2\n");
     }
@@ -252,7 +251,7 @@ fn main() {
     #[test]
     fn invert_match() {
         let (count, out) = search(
-            "Sherlock", &*SHERLOCK, |s| s.invert_match(true));
+            "Sherlock", SHERLOCK, |s| s.invert_match(true));
         assert_eq!(4, count);
         assert_eq!(out, "\
 /baz.rs:Holmeses, success in the province of detective work must always
@@ -264,7 +263,7 @@ fn main() {
 
     #[test]
     fn invert_match_line_numbers() {
-        let (count, out) = search("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search("Sherlock", SHERLOCK, |s| {
             s.invert_match(true).line_number(true)
         });
         assert_eq!(4, count);
@@ -278,7 +277,7 @@ fn main() {
 
     #[test]
     fn invert_match_count() {
-        let (count, out) = search("Sherlock", &*SHERLOCK, |s| {
+        let (count, out) = search("Sherlock", SHERLOCK, |s| {
             s.invert_match(true).count(true)
         });
         assert_eq!(4, count);
diff --git a/tests/hay.rs b/tests/hay.rs
new file mode 100644
index 00000000..74d2f6cc
--- /dev/null
+++ b/tests/hay.rs
@@ -0,0 +1,24 @@
+pub const SHERLOCK: &'static str = "\
+For the Doctor Watsons of this world, as opposed to the Sherlock
+Holmeses, success in the province of detective work must always
+be, to a very large extent, the result of luck. Sherlock Holmes
+can extract a clew from a wisp of straw or a flake of cigar ash;
+but Doctor Watson has to have it taken out for him and dusted,
+and exhibited clearly, with a label attached.
+";
+
+pub const CODE: &'static str = "\
+extern crate snap;
+
+use std::io;
+
+fn main() {
+    let stdin = io::stdin();
+    let stdout = io::stdout();
+
+    // Wrap the stdin reader in a Snappy reader.
+    let mut rdr = snap::Reader::new(stdin.lock());
+    let mut wtr = stdout.lock();
+    io::copy(&mut rdr, &mut wtr).expect(\"I/O operation failed\");
+}
+";
diff --git a/tests/tests.rs b/tests/tests.rs
new file mode 100644
index 00000000..608f3572
--- /dev/null
+++ b/tests/tests.rs
@@ -0,0 +1,563 @@
+/*!
+This module contains *integration* tests. Their purpose is to test the CLI
+interface. Namely, that passing a flag does what it says on the tin.
+
+Tests for more fine grained behavior (like the search or the globber) should be
+unit tests in their respective modules.
+*/
+
+#![allow(dead_code, unused_imports)]
+
+use std::process::Command;
+
+use workdir::WorkDir;
+
+mod hay;
+mod workdir;
+
+macro_rules! sherlock {
+    ($name:ident, $fun:expr) => {
+        sherlock!($name, "Sherlock", $fun);
+    };
+    ($name:ident, $query:expr, $fun:expr) => {
+        sherlock!($name, $query, "sherlock", $fun);
+    };
+    ($name:ident, $query:expr, $path:expr, $fun:expr) => {
+        #[test]
+        fn $name() {
+            let wd = WorkDir::new(stringify!($name));
+            wd.create("sherlock", hay::SHERLOCK);
+            let mut cmd = wd.command();
+            cmd.arg($query).arg($path);
+            $fun(wd, cmd);
+        }
+    };
+}
+
+sherlock!(single_file, |wd: WorkDir, mut cmd| {
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Doctor Watsons of this world, as opposed to the Sherlock
+be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(dir, "Sherlock", ".", |wd: WorkDir, mut cmd| {
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(line_numbers, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-n");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+1:For the Doctor Watsons of this world, as opposed to the Sherlock
+3:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(columns, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("--column");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+57:For the Doctor Watsons of this world, as opposed to the Sherlock
+49:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(with_filename, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-H");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(with_heading, |wd: WorkDir, mut cmd: Command| {
+    // This forces the issue since --with-filename is disabled by default
+    // when searching one fil.e
+    cmd.arg("--with-filename").arg("--heading");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+sherlock
+For the Doctor Watsons of this world, as opposed to the Sherlock
+be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(with_heading_default, "Sherlock", ".",
+|wd: WorkDir, mut cmd: Command| {
+    // Search two or more and get --with-filename enabled by default.
+    // Use -j1 to get deterministic results.
+    wd.create("foo", "Sherlock Holmes lives on Baker Street.");
+    cmd.arg("-j1").arg("--heading");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+foo
+Sherlock Holmes lives on Baker Street.
+
+sherlock
+For the Doctor Watsons of this world, as opposed to the Sherlock
+be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(inverted, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-v");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+Holmeses, success in the province of detective work must always
+can extract a clew from a wisp of straw or a flake of cigar ash;
+but Doctor Watson has to have it taken out for him and dusted,
+and exhibited clearly, with a label attached.
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(inverted_line_numbers, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-n").arg("-v");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+2:Holmeses, success in the province of detective work must always
+4:can extract a clew from a wisp of straw or a flake of cigar ash;
+5:but Doctor Watson has to have it taken out for him and dusted,
+6:and exhibited clearly, with a label attached.
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(case_insensitive, "sherlock", |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-i");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Doctor Watsons of this world, as opposed to the Sherlock
+be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(word, "as", |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-w");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Doctor Watsons of this world, as opposed to the Sherlock
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(literal, "()", "file", |wd: WorkDir, mut cmd: Command| {
+    wd.create("file", "blib\n()\nblab\n");
+    cmd.arg("-Q");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "()\n");
+});
+
+sherlock!(quiet, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-q");
+    let lines: String = wd.stdout(&mut cmd);
+    assert!(lines.is_empty());
+});
+
+sherlock!(replace, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-r").arg("FooBar");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Doctor Watsons of this world, as opposed to the FooBar
+be, to a very large extent, the result of luck. FooBar Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(replace_groups, "([A-Z][a-z]+) ([A-Z][a-z]+)",
+|wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-r").arg("$2, $1");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Watsons, Doctor of this world, as opposed to the Sherlock
+be, to a very large extent, the result of luck. Holmes, Sherlock
+but Watson, Doctor has to have it taken out for him and dusted,
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(replace_named_groups, "(?P<first>[A-Z][a-z]+) (?P<last>[A-Z][a-z]+)",
+|wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-r").arg("$last, $first");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Watsons, Doctor of this world, as opposed to the Sherlock
+be, to a very large extent, the result of luck. Holmes, Sherlock
+but Watson, Doctor has to have it taken out for him and dusted,
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(file_types, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.create("file.py", "Sherlock");
+    wd.create("file.rs", "Sherlock");
+    cmd.arg("-t").arg("rust");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "file.rs:Sherlock\n");
+});
+
+sherlock!(file_types_negate, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.remove("sherlock");
+    wd.create("file.py", "Sherlock");
+    wd.create("file.rs", "Sherlock");
+    cmd.arg("-T").arg("rust");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "file.py:Sherlock\n");
+});
+
+sherlock!(file_type_clear, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.create("file.py", "Sherlock");
+    wd.create("file.rs", "Sherlock");
+    cmd.arg("--type-clear").arg("rust").arg("-t").arg("rust");
+    wd.assert_err(&mut cmd);
+});
+
+sherlock!(file_type_add, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.create("file.py", "Sherlock");
+    wd.create("file.rs", "Sherlock");
+    wd.create("file.wat", "Sherlock");
+    cmd.arg("--type-add").arg("wat:*.wat").arg("-t").arg("wat");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "file.wat:Sherlock\n");
+});
+
+sherlock!(glob, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.create("file.py", "Sherlock");
+    wd.create("file.rs", "Sherlock");
+    cmd.arg("-g").arg("*.rs");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "file.rs:Sherlock\n");
+});
+
+sherlock!(glob_negate, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.remove("sherlock");
+    wd.create("file.py", "Sherlock");
+    wd.create("file.rs", "Sherlock");
+    cmd.arg("-g").arg("!*.rs");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "file.py:Sherlock\n");
+});
+
+sherlock!(after_context, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-A").arg("1");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Doctor Watsons of this world, as opposed to the Sherlock
+Holmeses, success in the province of detective work must always
+be, to a very large extent, the result of luck. Sherlock Holmes
+can extract a clew from a wisp of straw or a flake of cigar ash;
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(after_context_line_numbers, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-A").arg("1").arg("-n");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+1:For the Doctor Watsons of this world, as opposed to the Sherlock
+2-Holmeses, success in the province of detective work must always
+3:be, to a very large extent, the result of luck. Sherlock Holmes
+4-can extract a clew from a wisp of straw or a flake of cigar ash;
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(before_context, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-B").arg("1");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Doctor Watsons of this world, as opposed to the Sherlock
+Holmeses, success in the province of detective work must always
+be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(before_context_line_numbers, |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-B").arg("1").arg("-n");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+1:For the Doctor Watsons of this world, as opposed to the Sherlock
+2-Holmeses, success in the province of detective work must always
+3:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(context, "world|attached", |wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-C").arg("1");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+For the Doctor Watsons of this world, as opposed to the Sherlock
+Holmeses, success in the province of detective work must always
+--
+but Doctor Watson has to have it taken out for him and dusted,
+and exhibited clearly, with a label attached.
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(context_line_numbers, "world|attached",
+|wd: WorkDir, mut cmd: Command| {
+    cmd.arg("-C").arg("1").arg("-n");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+1:For the Doctor Watsons of this world, as opposed to the Sherlock
+2-Holmeses, success in the province of detective work must always
+--
+5-but Doctor Watson has to have it taken out for him and dusted,
+6:and exhibited clearly, with a label attached.
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(ignore_hidden, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.remove("sherlock");
+    wd.create(".sherlock", hay::SHERLOCK);
+    wd.assert_err(&mut cmd);
+});
+
+sherlock!(no_ignore_hidden, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.remove("sherlock");
+    wd.create(".sherlock", hay::SHERLOCK);
+
+    cmd.arg("--hidden");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+.sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+.sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(ignore_git, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.create(".gitignore", "sherlock\n");
+    wd.assert_err(&mut cmd);
+});
+
+sherlock!(ignore_ripgrep, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.create(".rgignore", "sherlock\n");
+    wd.assert_err(&mut cmd);
+});
+
+sherlock!(no_ignore, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.create(".gitignore", "sherlock\n");
+    cmd.arg("--no-ignore");
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(ignore_git_parent, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.remove("sherlock");
+    wd.create(".gitignore", "sherlock\n");
+    wd.create_dir(".git");
+    wd.create_dir("foo");
+    wd.create("foo/sherlock", hay::SHERLOCK);
+    // Even though we search in foo/, which has no .gitignore, ripgrep will
+    // search parent directories and respect the gitignore files found.
+    cmd.current_dir(wd.path().join("foo"));
+    wd.assert_err(&mut cmd);
+});
+
+sherlock!(ignore_git_parent_stop, "Sherlock", ".",
+|wd: WorkDir, mut cmd: Command| {
+    // This tests that searching parent directories for .gitignore files stops
+    // after it sees a .git directory. To test this, we create this directory
+    // hierarchy:
+    //
+    // .gitignore (contains `sherlock`)
+    // foo/
+    //   .git
+    //   bar/
+    //      sherlock
+    //
+    // And we perform the search inside `foo/bar/`. ripgrep will stop looking
+    // for .gitignore files after it sees `foo/.git/`, and therefore not
+    // respect the top-level `.gitignore` containing `sherlock`.
+    wd.remove("sherlock");
+    wd.create(".gitignore", "sherlock\n");
+    wd.create_dir("foo");
+    wd.create_dir("foo/.git");
+    wd.create_dir("foo/bar");
+    wd.create("foo/bar/sherlock", hay::SHERLOCK);
+    cmd.current_dir(wd.path().join("foo").join("bar"));
+
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(ignore_ripgrep_parent_no_stop, "Sherlock", ".",
+|wd: WorkDir, mut cmd: Command| {
+    // This is like the `ignore_git_parent_stop` test, except it checks that
+    // ripgrep *doesn't* stop checking for .rgignore files.
+    wd.remove("sherlock");
+    wd.create(".rgignore", "sherlock\n");
+    wd.create_dir("foo");
+    wd.create_dir("foo/.git");
+    wd.create_dir("foo/bar");
+    wd.create("foo/bar/sherlock", hay::SHERLOCK);
+    cmd.current_dir(wd.path().join("foo").join("bar"));
+    // The top-level .rgignore applies.
+    wd.assert_err(&mut cmd);
+});
+
+sherlock!(no_parent_ignore_git, "Sherlock", ".",
+|wd: WorkDir, mut cmd: Command| {
+    // Set up a directory hierarchy like this:
+    //
+    // .gitignore
+    // foo/
+    //   .gitignore
+    //   sherlock
+    //   watson
+    //
+    // Where `.gitignore` contains `sherlock` and `foo/.gitignore` contains
+    // `watson`.
+    //
+    // Now *do the search* from the foo directory. By default, ripgrep will
+    // search parent directories for .gitignore files. The --no-ignore-parent
+    // flag should prevent that. At the same time, the `foo/.gitignore` file
+    // will still be respected (since the search is happening in `foo/`).
+    //
+    // In other words, we should only see results from `sherlock`, not from
+    // `watson`.
+    wd.remove("sherlock");
+    wd.create(".gitignore", "sherlock\n");
+    wd.create_dir("foo");
+    wd.create("foo/.gitignore", "watson\n");
+    wd.create("foo/sherlock", hay::SHERLOCK);
+    wd.create("foo/watson", hay::SHERLOCK);
+    cmd.current_dir(wd.path().join("foo"));
+    cmd.arg("--no-ignore-parent");
+
+    let lines: String = wd.stdout(&mut cmd);
+    let expected = "\
+sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+    assert_eq!(lines, expected);
+});
+
+sherlock!(symlink_nofollow, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.remove("sherlock");
+    wd.create_dir("foo");
+    wd.create_dir("foo/bar");
+    wd.link("foo/baz", "foo/bar/baz");
+    wd.create_dir("foo/baz");
+    wd.create("foo/baz/sherlock", hay::SHERLOCK);
+    cmd.current_dir(wd.path().join("foo/bar"));
+    wd.assert_err(&mut cmd);
+});
+
+sherlock!(symlink_follow, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| {
+    wd.remove("sherlock");
+    wd.create_dir("foo");
+    wd.create_dir("foo/bar");
+    wd.create_dir("foo/baz");
+    wd.create("foo/baz/sherlock", hay::SHERLOCK);
+    wd.link("foo/baz", "foo/bar/baz");
+    cmd.arg("-L");
+    cmd.current_dir(wd.path().join("foo/bar"));
+
+    let lines: String = wd.stdout(&mut cmd);
+    if cfg!(windows) {
+        let expected = "\
+baz\\sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+baz\\sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+        assert_eq!(lines, expected);
+    } else {
+        let expected = "\
+baz/sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock
+baz/sherlock:be, to a very large extent, the result of luck. Sherlock Holmes
+";
+        assert_eq!(lines, expected);
+    }
+});
+
+#[test]
+fn binary_nosearch() {
+    let wd = WorkDir::new("binary_nosearch");
+    wd.create("file", "foo\x00bar\nfoo\x00baz\n");
+    let mut cmd = wd.command();
+    cmd.arg("foo").arg("file");
+    wd.assert_err(&mut cmd);
+}
+
+// The following two tests show a discrepancy in search results between
+// searching with memory mapped files and stream searching. Stream searching
+// uses a heuristic (that GNU grep also uses) where NUL bytes are replaced with
+// the EOL terminator, which tends to avoid allocating large amounts of memory
+// for really long "lines." The memory map searcher has no need to worry about
+// such things, and more than that, it would be pretty hard for it to match
+// the semantics of streaming search in this case.
+//
+// Binary files with lots of NULs aren't really part of the use case of ripgrep
+// (or any other grep-like tool for that matter), so we shouldn't feel too bad
+// about it.
+#[test]
+fn binary_search_mmap() {
+    let wd = WorkDir::new("binary_search_mmap");
+    wd.create("file", "foo\x00bar\nfoo\x00baz\n");
+    let mut cmd = wd.command();
+    cmd.arg("-a").arg("--mmap").arg("foo").arg("file");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "foo\x00bar\nfoo\x00baz\n");
+}
+
+#[test]
+fn binary_search_no_mmap() {
+    let wd = WorkDir::new("binary_search_no_mmap");
+    wd.create("file", "foo\x00bar\nfoo\x00baz\n");
+    let mut cmd = wd.command();
+    cmd.arg("-a").arg("--no-mmap").arg("foo").arg("file");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "foo\nfoo\n");
+}
+
+#[test]
+fn files() {
+    let wd = WorkDir::new("files");
+    wd.create("file", "");
+    wd.create_dir("dir");
+    wd.create("dir/file", "");
+
+    let mut cmd = wd.command();
+    cmd.arg("--files");
+    let lines: String = wd.stdout(&mut cmd);
+    assert_eq!(lines, "./file\n./dir/file\n");
+}
+
+#[test]
+fn type_list() {
+    let wd = WorkDir::new("type_list");
+
+    let mut cmd = wd.command();
+    cmd.arg("--type-list");
+    let lines: String = wd.stdout(&mut cmd);
+    // This can change over time, so just make sure we print something.
+    assert!(!lines.is_empty());
+}
diff --git a/tests/workdir.rs b/tests/workdir.rs
new file mode 100644
index 00000000..89b2da1d
--- /dev/null
+++ b/tests/workdir.rs
@@ -0,0 +1,189 @@
+use std::env;
+use std::error;
+use std::fmt;
+use std::fs::{self, File};
+use std::io::{self, Write};
+use std::path::{Path, PathBuf};
+use std::process;
+use std::str::FromStr;
+use std::sync::atomic::{ATOMIC_USIZE_INIT, AtomicUsize, Ordering};
+use std::thread;
+use std::time::Duration;
+
+static TEST_DIR: &'static str = "ripgrep-tests";
+static NEXT_ID: AtomicUsize = ATOMIC_USIZE_INIT;
+
+/// WorkDir represents a directory in which tests are run.
+///
+/// Directories are created from a global atomic counter to avoid duplicates.
+#[derive(Debug)]
+pub struct WorkDir {
+    /// The directory in which this test executable is running.
+    root: PathBuf,
+    /// The directory in which the test should run. If a test needs to create
+    /// files, they should go in here.
+    dir: PathBuf,
+}
+
+impl WorkDir {
+    /// Create a new test working directory with the given name. The name
+    /// does not need to be distinct for each invocation, but should correspond
+    /// to a logical grouping of tests.
+    pub fn new(name: &str) -> WorkDir {
+        let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
+        let root = env::current_exe().unwrap()
+            .parent().expect("executable's directory").to_path_buf();
+        let dir = root.join(TEST_DIR).join(name).join(&format!("{}", id));
+        nice_err(&dir, repeat(|| fs::create_dir_all(&dir)));
+        WorkDir {
+            root: root,
+            dir: dir,
+        }
+    }
+
+    /// Create a new file with the given name and contents in this directory.
+    pub fn create<P: AsRef<Path>>(&self, name: P, contents: &str) {
+        let path = self.dir.join(name);
+        let mut file = nice_err(&path, File::create(&path));
+        nice_err(&path, file.write_all(contents.as_bytes()));
+        nice_err(&path, file.flush());
+    }
+
+    /// Remove a file with the given name from this directory.
+    pub fn remove<P: AsRef<Path>>(&self, name: P) {
+        let path = self.dir.join(name);
+        nice_err(&path, fs::remove_file(&path));
+    }
+
+    /// Create a new directory with the given path (and any directories above
+    /// it) inside this directory.
+    pub fn create_dir<P: AsRef<Path>>(&self, path: P) {
+        let path = self.dir.join(path);
+        nice_err(&path, repeat(|| fs::create_dir_all(&path)));
+    }
+
+    /// Creates a new command that is set to use the ripgrep executable in
+    /// this working directory.
+    pub fn command(&self) -> process::Command {
+        let mut cmd = process::Command::new(&self.bin());
+        cmd.current_dir(&self.dir);
+        cmd
+    }
+
+    /// Returns the path to the ripgrep executable.
+    pub fn bin(&self) -> PathBuf {
+        self.root.join("rg")
+    }
+
+    /// Returns the path to this directory.
+    pub fn path(&self) -> &Path {
+        &self.dir
+    }
+
+    /// Creates a directory symlink to the src with the given target name
+    /// in this directory.
+    #[cfg(not(windows))]
+    pub fn link<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
+        use std::os::unix::fs::symlink;
+        let src = self.dir.join(src);
+        let target = self.dir.join(target);
+        let _ = fs::remove_file(&target);
+        nice_err(&target, symlink(&src, &target));
+    }
+
+    #[cfg(windows)]
+    pub fn link<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
+        use std::os::windows::fs::symlink_dir;
+        let src = self.dir.join(src);
+        let target = self.dir.join(target);
+        let _ = fs::remove_dir(&target);
+        nice_err(&target, symlink_dir(&src, &target));
+    }
+
+    /// Runs and captures the stdout of the given command.
+    ///
+    /// If the return type could not be created from a string, then this
+    /// panics.
+    pub fn stdout<E: fmt::Debug, T: FromStr<Err=E>>(
+        &self,
+        cmd: &mut process::Command,
+    ) -> T {
+        let o = self.output(cmd);
+        let stdout = String::from_utf8_lossy(&o.stdout);
+        match stdout.parse() {
+            Ok(t) => t,
+            Err(err) => {
+                panic!("could not convert from string: {:?}\n\n{}", err, stdout);
+            }
+        }
+    }
+
+    /// Gets the output of a command. If the command failed, then this panics.
+    pub fn output(&self, cmd: &mut process::Command) -> process::Output {
+        let o = cmd.output().unwrap();
+        if !o.status.success() {
+            let suggest =
+                if o.stderr.is_empty() {
+                    "\n\nDid your search end up with no results?".to_string()
+                } else {
+                    "".to_string()
+                };
+
+            panic!("\n\n==========\n\
+                    command failed but expected success!\
+                    {}\
+                    \n\ncommand: {:?}\
+                    \ncwd: {}\
+                    \n\nstatus: {}\
+                    \n\nstdout: {}\
+                    \n\nstderr: {}\
+                    \n\n==========\n",
+                   suggest, cmd, self.dir.display(), o.status,
+                   String::from_utf8_lossy(&o.stdout),
+                   String::from_utf8_lossy(&o.stderr));
+        }
+        o
+    }
+
+    /// Runs the given command and asserts that it resulted in an error exit
+    /// code.
+    pub fn assert_err(&self, cmd: &mut process::Command) {
+        let o = cmd.output().unwrap();
+        if o.status.success() {
+            panic!("\n\n===== {:?} =====\n\
+                    command succeeded but expected failure!\
+                    \n\ncwd: {}\
+                    \n\nstatus: {}\
+                    \n\nstdout: {}\n\nstderr: {}\
+                    \n\n=====\n",
+                   cmd, self.dir.display(), o.status,
+                   String::from_utf8_lossy(&o.stdout),
+                   String::from_utf8_lossy(&o.stderr));
+        }
+    }
+}
+
+fn nice_err<P: AsRef<Path>, T, E: error::Error>(
+    path: P,
+    res: Result<T, E>,
+) -> T {
+    match res {
+        Ok(t) => t,
+        Err(err) => {
+            panic!("{}: {:?}", path.as_ref().display(), err);
+        }
+    }
+}
+
+fn repeat<F: FnMut() -> io::Result<()>>(mut f: F) -> io::Result<()> {
+    let mut last_err = None;
+    for _ in 0..10 {
+        if let Err(err) = f() {
+            last_err = Some(err);
+            thread::sleep(Duration::from_millis(500));
+        } else {
+            return Ok(());
+        }
+    }
+    Err(last_err.unwrap())
+}