Repository: sharkdp/fd
Pull request #557 (2020-04-03 10:45:04)

Major code cleanup
- Proper error handling (instead of using `process::exit`)
- Clean up the module space, reduce number of different modules


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 976207d..0ce5b18 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -24,6 +24,11 @@ dependencies = [
  "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "anyhow"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -115,6 +120,7 @@ name = "fd-find"
 version = "7.5.0"
 dependencies = [
  "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)",
  "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
  "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "ctrlc 3.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -470,6 +476,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
 "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
 "checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+"checksum anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)" = "d9a60d744a80c30fcb657dfe2c1b22bcb3e814c1a1e3674f32bf5820b570fbff"
 "checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 "checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
 "checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
diff --git a/Cargo.toml b/Cargo.toml
index 63365cf..f1452ab 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,6 +45,7 @@ ctrlc = "3.1"
 humantime = "2.0"
 lscolors = "0.7"
 globset = "0.4"
+anyhow = "1.0"
 
 [dependencies.clap]
 version = "2.31.2"
diff --git a/build.rs b/build.rs
index ad7af7d..735ad74 100644
--- a/build.rs
+++ b/build.rs
@@ -1,17 +1,18 @@
-use clap::Shell;
 use std::fs;
-use std::io::{self, Write};
-use std::process::exit;
+
+use clap::Shell;
 
 include!("src/app.rs");
 
 fn main() {
-    match version_check::is_min_version("1.36") {
+    let min_version = "1.36";
+
+    match version_check::is_min_version(min_version) {
         Some(true) => {}
         // rustc version too small or can't figure it out
         _ => {
-            writeln!(&mut io::stderr(), "'fd' requires rustc >= 1.36").unwrap();
-            exit(1);
+            eprintln!("'fd' requires rustc >= {}", min_version);
+            std::process::exit(1);
         }
     }
 
diff --git a/src/error.rs b/src/error.rs
new file mode 100644
index 0000000..3501fee
--- /dev/null
+++ b/src/error.rs
@@ -0,0 +1,3 @@
+pub fn print_error(msg: impl Into<String>) {
+    eprintln!("[fd error]: {}", msg.into());
+}
diff --git a/src/exec/command.rs b/src/exec/command.rs
index 29dd219..ecdfe62 100644
--- a/src/exec/command.rs
+++ b/src/exec/command.rs
@@ -3,6 +3,7 @@ use std::io::Write;
 use std::process::Command;
 use std::sync::Mutex;
 
+use crate::error::print_error;
 use crate::exit_codes::ExitCode;
 
 /// Executes a command.
@@ -30,11 +31,11 @@ pub fn execute_command(mut cmd: Command, out_perm: &Mutex<()>) -> ExitCode {
             }
         }
         Err(ref why) if why.kind() == io::ErrorKind::NotFound => {
-            print_error!("Command not found: {:?}", cmd);
+            print_error(format!("Command not found: {:?}", cmd));
             ExitCode::GeneralError
         }
         Err(why) => {
-            print_error!("Problem while executing command: {}", why);
+            print_error(format!("Problem while executing command: {}", why));
             ExitCode::GeneralError
         }
     }
diff --git a/src/exec/job.rs b/src/exec/job.rs
index eafb0cd..eef042a 100644
--- a/src/exec/job.rs
+++ b/src/exec/job.rs
@@ -1,10 +1,13 @@
-use super::CommandTemplate;
-use crate::exit_codes::{merge_exitcodes, ExitCode};
-use crate::walk::WorkerResult;
 use std::path::PathBuf;
 use std::sync::mpsc::Receiver;
 use std::sync::{Arc, Mutex};
 
+use crate::error::print_error;
+use crate::exit_codes::{merge_exitcodes, ExitCode};
+use crate::walk::WorkerResult;
+
+use super::CommandTemplate;
+
 /// An event loop that listens for inputs from the `rx` receiver. Each received input will
 /// generate a command with the supplied command template. The generated command will then
 /// be executed, and this process will continue until the receiver's sender has closed.
@@ -25,7 +28,7 @@ pub fn job(
             Ok(WorkerResult::Entry(val)) => val,
             Ok(WorkerResult::Error(err)) => {
                 if show_filesystem_errors {
-                    print_error!("{}", err);
+                    print_error(err.to_string());
                 }
                 continue;
             }
@@ -50,7 +53,7 @@ pub fn batch(
         WorkerResult::Entry(val) => Some(val),
         WorkerResult::Error(err) => {
             if show_filesystem_errors {
-                print_error!("{}", err);
+                print_error(err.to_string());
             }
             None
         }
diff --git a/src/exec/mod.rs b/src/exec/mod.rs
index 5c27491..4c58287 100644
--- a/src/exec/mod.rs
+++ b/src/exec/mod.rs
@@ -1,4 +1,3 @@
-// TODO: Possible optimization could avoid pushing characters on a buffer.
 mod command;
 mod input;
 mod job;
@@ -9,6 +8,7 @@ use std::path::{Path, PathBuf};
 use std::process::{Command, Stdio};
 use std::sync::{Arc, Mutex};
 
+use anyhow::{anyhow, Result};
 use lazy_static::lazy_static;
 use regex::Regex;
 
@@ -47,17 +47,19 @@ impl CommandTemplate {
         Self::build(input, ExecutionMode::OneByOne)
     }
 
-    pub fn new_batch<I, S>(input: I) -> Result<CommandTemplate, &'static str>
+    pub fn new_batch<I, S>(input: I) -> Result<CommandTemplate>
     where
         I: IntoIterator<Item = S>,
         S: AsRef<str>,
     {
         let cmd = Self::build(input, ExecutionMode::Batch);
         if cmd.number_of_tokens() > 1 {
-            return Err("Only one placeholder allowed for batch commands");
+            return Err(anyhow!("Only one placeholder allowed for batch commands"));
         }
         if cmd.args[0].has_tokens() {
-            return Err("First argument of exec-batch is expected to be a fixed executable");
+            return Err(anyhow!(
+                "First argument of exec-batch is expected to be a fixed executable"
+            ));
         }
         Ok(cmd)
     }
diff --git a/src/fshelper/mod.rs b/src/filesystem.rs
similarity index 54%
rename from src/fshelper/mod.rs
rename to src/filesystem.rs
index 933416a..de926ac 100644
--- a/src/fshelper/mod.rs
+++ b/src/filesystem.rs
@@ -1,9 +1,11 @@
+use std::borrow::Cow;
 use std::env::current_dir;
+use std::ffi::OsStr;
 use std::fs;
 use std::io;
 #[cfg(any(unix, target_os = "redox"))]
 use std::os::unix::fs::PermissionsExt;
-use std::path::{Path, PathBuf};
+use std::path::{Component, Path, PathBuf};
 
 use crate::walk;
 
@@ -64,3 +66,49 @@ pub fn is_empty(entry: &walk::DirEntry) -> bool {
         false
     }
 }
+
+#[cfg(any(unix, target_os = "redox"))]
+pub fn osstr_to_bytes(input: &OsStr) -> Cow<[u8]> {
+    use std::os::unix::ffi::OsStrExt;
+    Cow::Borrowed(input.as_bytes())
+}
+
+#[cfg(windows)]
+pub fn osstr_to_bytes(input: &OsStr) -> Cow<[u8]> {
+    let string = input.to_string_lossy();
+
+    match string {
+        Cow::Owned(string) => Cow::Owned(string.into_bytes()),
+        Cow::Borrowed(string) => Cow::Borrowed(string.as_bytes()),
+    }
+}
+
+/// Remove the `./` prefix from a path.
+pub fn strip_current_dir(pathbuf: &PathBuf) -> &Path {
+    let mut iter = pathbuf.components();
+    let mut iter_next = iter.clone();
+    if iter_next.next() == Some(Component::CurDir) {
+        iter.next();
+    }
+    iter.as_path()
+}
+
+#[cfg(test)]
+mod tests {
+    use super::strip_current_dir;
+    use std::path::{Path, PathBuf};
+
+    #[test]
+    fn strip_current_dir_basic() {
+        assert_eq!(strip_current_dir(&PathBuf::from("./foo")), Path::new("foo"));
+        assert_eq!(strip_current_dir(&PathBuf::from("foo")), Path::new("foo"));
+        assert_eq!(
+            strip_current_dir(&PathBuf::from("./foo/bar/baz")),
+            Path::new("foo/bar/baz")
+        );
+        assert_eq!(
+            strip_current_dir(&PathBuf::from("foo/bar/baz")),
+            Path::new("foo/bar/baz")
+        );
+    }
+}
diff --git a/src/internal/file_types.rs b/src/filetypes.rs
similarity index 100%
rename from src/internal/file_types.rs
rename to src/filetypes.rs
diff --git a/src/internal/filter/mod.rs b/src/filter/mod.rs
similarity index 100%
rename from src/internal/filter/mod.rs
rename to src/filter/mod.rs
diff --git a/src/internal/filter/size.rs b/src/filter/size.rs
similarity index 100%
rename from src/internal/filter/size.rs
rename to src/filter/size.rs
diff --git a/src/internal/filter/time.rs b/src/filter/time.rs
similarity index 100%
rename from src/internal/filter/time.rs
rename to src/filter/time.rs
diff --git a/src/internal/mod.rs b/src/internal/mod.rs
deleted file mode 100644
index 3235f45..0000000
--- a/src/internal/mod.rs
+++ /dev/null
@@ -1,189 +0,0 @@
-use std::borrow::Cow;
-use std::ffi::{OsStr, OsString};
-
-use regex_syntax::hir::Hir;
-use regex_syntax::ParserBuilder;
-
-pub use self::file_types::FileTypes;
-
-mod file_types;
-pub mod filter;
-pub mod opts;
-
-macro_rules! print_error {
-    ($($arg:tt)*) => (eprintln!("[fd error]: {}", format!($($arg)*)))
-}
-
-macro_rules! print_error_and_exit {
-    ($($arg:tt)*) => {
-        print_error!($($arg)*);
-        ::std::process::exit(1);
-    };
-}
-
-#[cfg(any(unix, target_os = "redox"))]
-pub fn osstr_to_bytes(input: &OsStr) -> Cow<[u8]> {
-    use std::os::unix::ffi::OsStrExt;
-    Cow::Borrowed(input.as_bytes())
-}
-
-#[cfg(windows)]
-pub fn osstr_to_bytes(input: &OsStr) -> Cow<[u8]> {
-    let string = input.to_string_lossy();
-
-    match string {
-        Cow::Owned(string) => Cow::Owned(string.into_bytes()),
-        Cow::Borrowed(string) => Cow::Borrowed(string.as_bytes()),
-    }
-}
-
-/// Determine if a regex pattern contains a literal uppercase character.
-pub fn pattern_has_uppercase_char(pattern: &str) -> bool {
-    let mut parser = ParserBuilder::new().allow_invalid_utf8(true).build();
-
-    parser
-        .parse(pattern)
-        .map(|hir| hir_has_uppercase_char(&hir))
-        .unwrap_or(false)
-}
-
-/// Determine if a regex expression contains a literal uppercase character.
-fn hir_has_uppercase_char(hir: &Hir) -> bool {
-    use regex_syntax::hir::*;
-
-    match *hir.kind() {
-        HirKind::Literal(Literal::Unicode(c)) => c.is_uppercase(),
-        HirKind::Literal(Literal::Byte(b)) => char::from(b).is_uppercase(),
-        HirKind::Class(Class::Unicode(ref ranges)) => ranges
-            .iter()
-            .any(|r| r.start().is_uppercase() || r.end().is_uppercase()),
-        HirKind::Class(Class::Bytes(ref ranges)) => ranges
-            .iter()
-            .any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()),
-        HirKind::Group(Group { ref hir, .. }) | HirKind::Repetition(Repetition { ref hir, .. }) => {
-            hir_has_uppercase_char(hir)
-        }
-        HirKind::Concat(ref hirs) | HirKind::Alternation(ref hirs) => {
-            hirs.iter().any(hir_has_uppercase_char)
-        }
-        _ => false,
-    }
-}
-
-/// Maximum size of the output buffer before flushing results to the console
-pub const MAX_BUFFER_LENGTH: usize = 1000;
-
-/// Traverse args_os, looking for -exec and replacing it with --exec.
-///
-/// # Returns
-///
-/// * The args, with substitution if required
-pub fn transform_args_with_exec<I>(original: I) -> Vec<OsString>
-where
-    I: Iterator<Item = OsString>,
-{
-    let mut in_exec_opt = false;
-    let target = OsString::from("-exec");
-    let long_start = OsString::from("--exec");
-    let short_start = OsString::from("-x");
-    let exec_end = OsString::from(";");
-
-    original.fold(vec![], |mut args, curr| {
-        if in_exec_opt {
-            if curr == exec_end {
-                in_exec_opt = false;
-            }
-            args.push(curr);
-            return args;
-        }
-
-        if curr == target || curr == long_start || curr == short_start {
-            args.push(if curr == target {
-                OsString::from("--exec")
-            } else {
-                curr
-            });
-            in_exec_opt = true;
-        } else {
-            args.push(curr);
-        }
-        args
-    })
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    fn oss_vec(strs: &[&str]) -> Vec<OsString> {
-        strs.into_iter().map(OsString::from).collect()
-    }
-
-    /// Ensure that -exec gets transformed into --exec
-    #[test]
-    fn normal_exec_substitution() {
-        let original = oss_vec(&["fd", "foo", "-exec", "cmd"]);
-        let expected = oss_vec(&["fd", "foo", "--exec", "cmd"]);
-        let actual = transform_args_with_exec(original.into_iter());
-        assert_eq!(expected, actual);
-    }
-    /// Ensure that --exec is not touched
-    #[test]
-    fn passthru_of_original_exec() {
-        let original = oss_vec(&["fd", "foo", "--exec", "cmd"]);
-        let expected = oss_vec(&["fd", "foo", "--exec", "cmd"]);
-        let actual = transform_args_with_exec(original.into_iter());
-        assert_eq!(expected, actual);
-    }
-    #[test]
-    fn temp_check_that_exec_context_observed() {
-        let original = oss_vec(&[
-            "fd",
-            "foo",
-            "-exec",
-            "cmd",
-            "-exec",
-            "ls",
-            ";",
-            "-exec",
-            "rm",
-            ";",
-            "--exec",
-            "find",
-            "-exec",
-            "rm",
-            ";",
-            "-x",
-            "foo",
-            "-exec",
-            "something",
-            ";",
-            "-exec",
-        ]);
-        let expected = oss_vec(&[
-            "fd",
-            "foo",
-            "--exec",
-            "cmd",
-            "-exec",
-            "ls",
-            ";",
-            "--exec",
-            "rm",
-            ";",
-            "--exec",
-            "find",
-            "-exec",
-            "rm",
-            ";",
-            "-x",
-            "foo",
-            "-exec",
-            "something",
-            ";",
-            "--exec",
-        ]);
-        let actual = transform_args_with_exec(original.into_iter());
-        assert_eq!(expected, actual);
-    }
-}
diff --git a/src/main.rs b/src/main.rs
index fbf7f24..3952803 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,11 +1,13 @@
-#[macro_use]
-mod internal;
-
 mod app;
+mod error;
 mod exec;
 mod exit_codes;
-pub mod fshelper;
+mod filesystem;
+mod filetypes;
+mod filter;
+mod options;
 mod output;
+mod regex_helper;
 mod walk;
 
 use std::env;
@@ -14,54 +16,54 @@ use std::process;
 use std::sync::Arc;
 use std::time;
 
+use anyhow::{anyhow, Context, Result};
 use atty::Stream;
 use globset::Glob;
 use lscolors::LsColors;
 use regex::bytes::{RegexBuilder, RegexSetBuilder};
 
 use crate::exec::CommandTemplate;
-use crate::internal::{
-    filter::{SizeFilter, TimeFilter},
-    opts::FdOptions,
-    pattern_has_uppercase_char, transform_args_with_exec, FileTypes,
-};
+use crate::exit_codes::ExitCode;
+use crate::filetypes::FileTypes;
+use crate::filter::{SizeFilter, TimeFilter};
+use crate::options::Options;
+use crate::regex_helper::pattern_has_uppercase_char;
 
 // We use jemalloc for performance reasons, see https://github.com/sharkdp/fd/pull/481
 #[cfg(all(not(windows), not(target_env = "musl")))]
 #[global_allocator]
 static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
 
-fn main() {
-    let checked_args = transform_args_with_exec(env::args_os());
-    let matches = app::build_app().get_matches_from(checked_args);
+fn run() -> Result<ExitCode> {
+    let matches = app::build_app().get_matches_from(env::args_os());
 
     // Set the current working directory of the process
     if let Some(base_directory) = matches.value_of("base-directory") {
-        let basedir = Path::new(base_directory);
-        if !fshelper::is_dir(basedir) {
-            print_error_and_exit!(
-                "The '--base-directory' path ('{}') is not a directory.",
-                basedir.to_string_lossy()
-            );
-        }
-        if let Err(e) = env::set_current_dir(basedir) {
-            print_error_and_exit!(
-                "Could not set '{}' as the current working directory: {}",
-                basedir.to_string_lossy(),
-                e
-            );
+        let base_directory = Path::new(base_directory);
+        if !filesystem::is_dir(base_directory) {
+            return Err(anyhow!(
+                "The '--base-directory' path '{}' is not a directory.",
+                base_directory.to_string_lossy()
+            ));
         }
+        env::set_current_dir(base_directory).with_context(|| {
+            format!(
+                "Could not set '{}' as the current working directory.",
+                base_directory.to_string_lossy()
+            )
+        })?;
+    }
+
+    let current_directory = Path::new(".");
+    if !filesystem::is_dir(current_directory) {
+        return Err(anyhow!(
+            "Could not retrieve current directory (has it been deleted?)."
+        ));
     }
 
     // Get the search pattern
     let pattern = matches.value_of("pattern").unwrap_or("");
 
-    // Get the current working directory
-    let current_dir = Path::new(".");
-    if !fshelper::is_dir(current_dir) {
-        print_error_and_exit!("Could not get current directory.");
-    }
-
     // Get one or more root directories to search.
     let mut dir_vec: Vec<_> = match matches
         .values_of("path")
@@ -70,16 +72,17 @@ fn main() {
         Some(paths) => paths
             .map(|path| {
                 let path_buffer = PathBuf::from(path);
-                if !fshelper::is_dir(&path_buffer) {
-                    print_error_and_exit!(
-                        "'{}' is not a directory.",
+                if filesystem::is_dir(&path_buffer) {
+                    Ok(path_buffer)
+                } else {
+                    Err(anyhow!(
+                        "Search path '{}' is not a directory.",
                         path_buffer.to_string_lossy()
-                    );
+                    ))
                 }
-                path_buffer
             })
-            .collect::<Vec<_>>(),
-        None => vec![current_dir.to_path_buf()],
+            .collect::<Result<Vec<_>>>()?,
+        None => vec![current_directory.to_path_buf()],
     };
 
     if matches.is_present("absolute-path") {
@@ -88,7 +91,7 @@ fn main() {
             .map(|path_buffer| {
                 path_buffer
                     .canonicalize()
-                    .and_then(|pb| fshelper::absolute_path(pb.as_path()))
+                    .and_then(|pb| filesystem::absolute_path(pb.as_path()))
                     .unwrap()
             })
             .collect();
@@ -97,27 +100,22 @@ fn main() {
     // Detect if the user accidentally supplied a path instead of a search pattern
     if !matches.is_present("full-path")
         && pattern.contains(std::path::MAIN_SEPARATOR)
-        && fshelper::is_dir(Path::new(pattern))
+        && filesystem::is_dir(Path::new(pattern))
     {
-        print_error_and_exit!(
+        return Err(anyhow!(
             "The search pattern '{pattern}' contains a path-separation character ('{sep}') \
              and will not lead to any search results.\n\n\
              If you want to search for all files inside the '{pattern}' directory, use a match-all pattern:\n\n  \
              fd . '{pattern}'\n\n\
-             Instead, if you want to search for the pattern in the full path, use:\n\n  \
+             Instead, if you want your pattern to match the full file path, use:\n\n  \
              fd --full-path '{pattern}'",
             pattern = pattern,
             sep = std::path::MAIN_SEPARATOR,
-        );
+        ));
     }
 
     let pattern_regex = if matches.is_present("glob") {
-        let glob = match Glob::new(pattern) {
-            Ok(glob) => glob,
-            Err(e) => {
-                print_error_and_exit!("{}", e);
-            }
-        };
+        let glob = Glob::new(pattern)?;
         glob.regex().to_owned()
     } else if matches.is_present("fixed-strings") {
         // Treat pattern as literal string if '--fixed-strings' is used
@@ -148,48 +146,39 @@ fn main() {
         None
     };
 
-    let command = matches
-        .values_of("exec")
-        .map(CommandTemplate::new)
-        .or_else(|| {
-            matches.values_of("exec-batch").map(|m| {
-                CommandTemplate::new_batch(m).unwrap_or_else(|e| {
-                    print_error_and_exit!("{}", e);
-                })
-            })
-        })
-        .or_else(|| {
-            if matches.is_present("list-details") {
-                let color = matches.value_of("color").unwrap_or("auto");
-                let color_arg = ["--color=", color].concat();
+    let command = if let Some(args) = matches.values_of("exec") {
+        Some(CommandTemplate::new(args))
+    } else if let Some(args) = matches.values_of("exec-batch") {
+        Some(CommandTemplate::new_batch(args)?)
+    } else if matches.is_present("list-details") {
+        let color = matches.value_of("color").unwrap_or("auto");
+        let color_arg = ["--color=", color].concat();
 
-                Some(
-                    CommandTemplate::new_batch(&[
-                        "ls",
-                        "-l",               // long listing format
-                        "--human-readable", // human readable file sizes
-                        "--directory",      // list directories themselves, not their contents
-                        &color_arg,         // enable colorized output, if enabled
-                    ])
-                    .unwrap(),
-                )
-            } else {
-                None
-            }
-        });
+        Some(
+            CommandTemplate::new_batch(&[
+                "ls",
+                "-l",               // long listing format
+                "--human-readable", // human readable file sizes
+                "--directory",      // list directories themselves, not their contents
+                &color_arg,         // enable colorized output, if enabled
+            ])
+            .unwrap(),
+        )
+    } else {
+        None
+    };
 
-    let size_limits: Vec<SizeFilter> = matches
-        .values_of("size")
-        .map(|v| {
-            v.map(|sf| {
-                if let Some(f) = SizeFilter::from_string(sf) {
-                    return f;
-                }
-                print_error_and_exit!("'{}' is not a valid size constraint. See 'fd --help'.", sf);
-            })
-            .collect()
+    let size_limits = if let Some(vs) = matches.values_of("size") {
+        vs.map(|sf| {
+            SizeFilter::from_string(sf).ok_or(anyhow!(
+                "'{}' is not a valid size constraint. See 'fd --help'.",
+                sf
+            ))
         })
-        .unwrap_or_else(|| vec![]);
+        .collect::<Result<Vec<_>>>()?
+    } else {
+        vec![]
+    };
 
     let now = time::SystemTime::now();
     let mut time_constraints: Vec<TimeFilter> = Vec::new();
@@ -197,18 +186,24 @@ fn main() {
         if let Some(f) = TimeFilter::after(&now, t) {
             time_constraints.push(f);
         } else {
-            print_error_and_exit!("'{}' is not a valid date or duration. See 'fd --help'.", t);
+            return Err(anyhow!(
+                "'{}' is not a valid date or duration. See 'fd --help'.",
+                t
+            ));
         }
     }
     if let Some(t) = matches.value_of("changed-before") {
         if let Some(f) = TimeFilter::before(&now, t) {
             time_constraints.push(f);
         } else {
-            print_error_and_exit!("'{}' is not a valid date or duration. See 'fd --help'.", t);
+            return Err(anyhow!(
+                "'{}' is not a valid date or duration. See 'fd --help'.",
+                t
+            ));
         }
     }
 
-    let config = FdOptions {
+    let config = Options {
         case_sensitive,
         search_full_path: matches.is_present("full-path"),
         ignore_hidden: !(matches.is_present("hidden")
@@ -263,20 +258,17 @@ fn main() {
 
             file_types
         }),
-        extensions: matches.values_of("extension").map(|exts| {
-            let patterns = exts
-                .map(|e| e.trim_start_matches('.'))
-                .map(|e| format!(r".\.{}$", regex::escape(e)));
-            match RegexSetBuilder::new(patterns)
-                .case_insensitive(true)
-                .build()
-            {
-                Ok(re) => re,
-                Err(err) => {
-                    print_error_and_exit!("{}", err.to_string());
-                }
-            }
-        }),
+        extensions: matches
+            .values_of("extension")
+            .map(|exts| {
+                let patterns = exts
+                    .map(|e| e.trim_start_matches('.'))
+                    .map(|e| format!(r".\.{}$", regex::escape(e)));
+                RegexSetBuilder::new(patterns)
+                    .case_insensitive(true)
+                    .build()
+            })
+            .transpose()?,
         command: command.map(Arc::new),
         exclude_patterns: matches
             .values_of("exclude")
@@ -296,21 +288,31 @@ fn main() {
             .filter(|&n| n != 0),
     };
 
-    match RegexBuilder::new(&pattern_regex)
+    let re = RegexBuilder::new(&pattern_regex)
         .case_insensitive(!config.case_sensitive)
         .dot_matches_new_line(true)
         .build()
-    {
-        Ok(re) => {
-            let exit_code = walk::scan(&dir_vec, Arc::new(re), Arc::new(config));
+        .map_err(|e| {
+            anyhow!(
+                "{}\n\nNote: You can use the '--fixed-strings' option to search for a \
+                 literal string instead of a regular expression. Alternatively, you can \
+                 also use the '--glob' option to match on a glob pattern.",
+                e.to_string()
+            )
+        })?;
+
+    walk::scan(&dir_vec, Arc::new(re), Arc::new(config))
+}
+
+fn main() {
+    let result = run();
+    match result {
+        Ok(exit_code) => {
             process::exit(exit_code.into());
         }
         Err(err) => {
-            print_error_and_exit!(
-                "{}\nHint: You can use the '--fixed-strings' option to search for a \
-                 literal string instead of a regular expression",
-                err.to_string()
-            );
+            eprintln!("[fd error]: {}", err);
+            process::exit(ExitCode::GeneralError.into());
         }
     }
 }
diff --git a/src/internal/opts.rs b/src/options.rs
similarity index 96%
rename from src/internal/opts.rs
rename to src/options.rs
index 88ace4e..d0620c9 100644
--- a/src/internal/opts.rs
+++ b/src/options.rs
@@ -1,14 +1,14 @@
-use crate::exec::CommandTemplate;
-use crate::internal::{
-    filter::{SizeFilter, TimeFilter},
-    FileTypes,
-};
+use std::{path::PathBuf, sync::Arc, time::Duration};
+
 use lscolors::LsColors;
 use regex::bytes::RegexSet;
-use std::{path::PathBuf, sync::Arc, time::Duration};
+
+use crate::exec::CommandTemplate;
+use crate::filetypes::FileTypes;
+use crate::filter::{SizeFilter, TimeFilter};
 
 /// Configuration options for *fd*.
-pub struct FdOptions {
+pub struct Options {
     /// Whether the search is case-sensitive or case-insensitive.
     pub case_sensitive: bool,
 
diff --git a/src/output.rs b/src/output.rs
index a38f9f5..68c9366 100644
--- a/src/output.rs
+++ b/src/output.rs
@@ -1,30 +1,21 @@
-use crate::exit_codes::ExitCode;
-use crate::internal::opts::FdOptions;
-use lscolors::{LsColors, Style};
-
 use std::borrow::Cow;
 use std::io::{self, StdoutLock, Write};
-use std::path::{Component, Path, PathBuf};
+use std::path::{Path, PathBuf};
 use std::process;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::Arc;
 
 use ansi_term;
+use lscolors::{LsColors, Style};
 
-/// Remove the `./` prefix from a path.
-fn strip_current_dir(pathbuf: &PathBuf) -> &Path {
-    let mut iter = pathbuf.components();
-    let mut iter_next = iter.clone();
-    if iter_next.next() == Some(Component::CurDir) {
-        iter.next();
-    }
-    iter.as_path()
-}
+use crate::exit_codes::ExitCode;
+use crate::filesystem::strip_current_dir;
+use crate::options::Options;
 
 pub fn print_entry(
     stdout: &mut StdoutLock,
     entry: &PathBuf,
-    config: &FdOptions,
+    config: &Options,
     wants_to_quit: &Arc<AtomicBool>,
 ) {
     let path = if entry.is_absolute() {
@@ -45,7 +36,7 @@ pub fn print_entry(
     }
 }
 
-fn replace_path_separator<'a>(config: &FdOptions, path: &mut Cow<'a, str>) {
+fn replace_path_separator<'a>(config: &Options, path: &mut Cow<'a, str>) {
     match &config.path_separator {
         None => {}
         Some(sep) => {
@@ -57,7 +48,7 @@ fn replace_path_separator<'a>(config: &FdOptions, path: &mut Cow<'a, str>) {
 fn print_entry_colorized(
     stdout: &mut StdoutLock,
     path: &Path,
-    config: &FdOptions,
+    config: &Options,
     ls_colors: &LsColors,
     wants_to_quit: &Arc<AtomicBool>,
 ) -> io::Result<()> {
@@ -89,7 +80,7 @@ fn print_entry_colorized(
 fn print_entry_uncolorized(
     stdout: &mut StdoutLock,
     path: &Path,
-    config: &FdOptions,
+    config: &Options,
 ) -> io::Result<()> {
     let separator = if config.null_separator { "\0" } else { "\n" };
 
diff --git a/src/regex_helper.rs b/src/regex_helper.rs
new file mode 100644
index 0000000..4cadd96
--- /dev/null
+++ b/src/regex_helper.rs
@@ -0,0 +1,52 @@
+use regex_syntax::hir::Hir;
+use regex_syntax::ParserBuilder;
+
+/// Determine if a regex pattern contains a literal uppercase character.
+pub fn pattern_has_uppercase_char(pattern: &str) -> bool {
+    let mut parser = ParserBuilder::new().allow_invalid_utf8(true).build();
+
+    parser
+        .parse(pattern)
+        .map(|hir| hir_has_uppercase_char(&hir))
+        .unwrap_or(false)
+}
+
+/// Determine if a regex expression contains a literal uppercase character.
+fn hir_has_uppercase_char(hir: &Hir) -> bool {
+    use regex_syntax::hir::*;
+
+    match *hir.kind() {
+        HirKind::Literal(Literal::Unicode(c)) => c.is_uppercase(),
+        HirKind::Literal(Literal::Byte(b)) => char::from(b).is_uppercase(),
+        HirKind::Class(Class::Unicode(ref ranges)) => ranges
+            .iter()
+            .any(|r| r.start().is_uppercase() || r.end().is_uppercase()),
+        HirKind::Class(Class::Bytes(ref ranges)) => ranges
+            .iter()
+            .any(|r| char::from(r.start()).is_uppercase() || char::from(r.end()).is_uppercase()),
+        HirKind::Group(Group { ref hir, .. }) | HirKind::Repetition(Repetition { ref hir, .. }) => {
+            hir_has_uppercase_char(hir)
+        }
+        HirKind::Concat(ref hirs) | HirKind::Alternation(ref hirs) => {
+            hirs.iter().any(hir_has_uppercase_char)
+        }
+        _ => false,
+    }
+}
+
+#[test]
+fn pattern_has_uppercase_char_simple() {
+    assert!(pattern_has_uppercase_char("A"));
+    assert!(pattern_has_uppercase_char("foo.EXE"));
+
+    assert!(!pattern_has_uppercase_char("a"));
+    assert!(!pattern_has_uppercase_char("foo.exe123"));
+}
+
+#[test]
+fn pattern_has_uppercase_char_advanced() {
+    assert!(pattern_has_uppercase_char("foo.[a-zA-Z]"));
+
+    assert!(!pattern_has_uppercase_char(r"\Acargo"));
+    assert!(!pattern_has_uppercase_char(r"carg\x6F"));
+}
diff --git a/src/walk.rs b/src/walk.rs
index 0d3d859..2181a41 100644
--- a/src/walk.rs
+++ b/src/walk.rs
@@ -1,9 +1,3 @@
-use crate::exec;
-use crate::exit_codes::{merge_exitcodes, ExitCode};
-use crate::fshelper;
-use crate::internal::{opts::FdOptions, osstr_to_bytes, MAX_BUFFER_LENGTH};
-use crate::output;
-
 use std::borrow::Cow;
 use std::ffi::OsStr;
 use std::fs::{FileType, Metadata};
@@ -16,10 +10,18 @@ use std::sync::{Arc, Mutex};
 use std::thread;
 use std::time;
 
+use anyhow::{anyhow, Result};
 use ignore::overrides::OverrideBuilder;
 use ignore::{self, WalkBuilder};
 use regex::bytes::Regex;
 
+use crate::error::print_error;
+use crate::exec;
+use crate::exit_codes::{merge_exitcodes, ExitCode};
+use crate::filesystem;
+use crate::options::Options;
+use crate::output;
+
 /// The receiver thread can either be buffering results or directly streaming to the console.
 enum ReceiverMode {
     /// Receiver is still buffering in order to sort the results, if the search finishes fast
@@ -36,12 +38,15 @@ pub enum WorkerResult {
     Error(ignore::Error),
 }
 
+/// Maximum size of the output buffer before flushing results to the console
+pub const MAX_BUFFER_LENGTH: usize = 1000;
+
 /// Recursively scan the given search path for files / pathnames matching the pattern.
 ///
 /// If the `--exec` argument was supplied, this will create a thread pool for executing
 /// jobs in parallel from a given command line and the discovered paths. Otherwise, each
 /// path will simply be written to standard output.
-pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) -> ExitCode {
+pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<Options>) -> Result<ExitCode> {
     let mut path_iter = path_vec.iter();
     let first_path_buf = path_iter
         .next()
@@ -51,14 +56,13 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) -
     let mut override_builder = OverrideBuilder::new(first_path_buf.as_path());
 
     for pattern in &config.exclude_patterns {
-        let res = override_builder.add(pattern);
-        if res.is_err() {
-            print_error_and_exit!("Malformed exclude pattern '{}'", pattern);
-        }
+        override_builder
+            .add(pattern)
+            .map_err(|e| anyhow!("Malformed exclude pattern: {}", e))?;
     }
-    let overrides = override_builder.build().unwrap_or_else(|_| {
-        print_error_and_exit!("Mismatch in exclude patterns");
-    });
+    let overrides = override_builder
+        .build()
+        .map_err(|_| anyhow!("Mismatch in exclude patterns"))?;
 
     let mut walker = WalkBuilder::new(first_path_buf.as_path());
     walker
@@ -83,13 +87,10 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) -
         match result {
             Some(ignore::Error::Partial(_)) => (),
             Some(err) => {
-                print_error!(
-                    "{}",
-                    format!(
-                        "Malformed pattern in custom ignore file. {}.",
-                        err.to_string()
-                    )
-                );
+                print_error(format!(
+                    "Malformed pattern in custom ignore file. {}.",
+                    err.to_string()
+                ));
             }
             None => (),
         }
@@ -125,14 +126,14 @@ pub fn scan(path_vec: &[PathBuf], pattern: Arc<Regex>, config: Arc<FdOptions>) -
     let exit_code = receiver_thread.join().unwrap();
 
     if wants_to_quit.load(Ordering::Relaxed) {
-        process::exit(ExitCode::KilledBySigint.into());
+        Ok(ExitCode::KilledBySigint)
+    } else {
+        Ok(exit_code)
     }
-
-    exit_code
 }
 
 fn spawn_receiver(
-    config: &Arc<FdOptions>,
+    config: &Arc<Options>,
     wants_to_quit: &Arc<AtomicBool>,
     rx: Receiver<WorkerResult>,
 ) -> thread::JoinHandle<ExitCode> {
@@ -228,7 +229,7 @@ fn spawn_receiver(
                     }
                     WorkerResult::Error(err) => {
                         if show_filesystem_errors {
-                            print_error!("{}", err);
+                            print_error(err.to_string());
                         }
                     }
                 }
@@ -285,7 +286,7 @@ impl DirEntry {
 }
 
 fn spawn_senders(
-    config: &Arc<FdOptions>,
+    config: &Arc<Options>,
     wants_to_quit: &Arc<AtomicBool>,
     pattern: Arc<Regex>,
     parallel_walker: ignore::WalkParallel,
@@ -341,12 +342,9 @@ fn spawn_senders(
             let entry_path = entry.path();
 
             let search_str: Cow<OsStr> = if config.search_full_path {
-                match fshelper::path_absolute_form(entry_path) {
-                    Ok(path_abs_buf) => Cow::Owned(path_abs_buf.as_os_str().to_os_string()),
-                    Err(_) => {
-                        print_error_and_exit!("Unable to retrieve absolute path.");
-                    }
-                }
+                let path_abs_buf = filesystem::path_absolute_form(entry_path)
+                    .expect("Retrieving absolute path succeeds");
+                Cow::Owned(path_abs_buf.as_os_str().to_os_string())
             } else {
                 match entry_path.file_name() {
                     Some(filename) => Cow::Borrowed(filename),
@@ -358,14 +356,14 @@ fn spawn_senders(
                 }
             };
 
-            if !pattern.is_match(&osstr_to_bytes(search_str.as_ref())) {
+            if !pattern.is_match(&filesystem::osstr_to_bytes(search_str.as_ref())) {
                 return ignore::WalkState::Continue;
             }
 
             // Filter out unwanted extensions.
             if let Some(ref exts_regex) = config.extensions {
                 if let Some(path_str) = entry_path.file_name() {
-                    if !exts_regex.is_match(&osstr_to_bytes(path_str)) {
+                    if !exts_regex.is_match(&filesystem::osstr_to_bytes(path_str)) {
                         return ignore::WalkState::Continue;
                     }
                 } else {
@@ -382,9 +380,9 @@ fn spawn_senders(
                         || (file_types.executables_only
                             && !entry
                                 .metadata()
-                                .map(|m| fshelper::is_executable(&m))
+                                .map(|m| filesystem::is_executable(&m))
                                 .unwrap_or(false))
-                        || (file_types.empty_only && !fshelper::is_empty(&entry))
+                        || (file_types.empty_only && !filesystem::is_empty(&entry))
                         || !(entry_type.is_file() || entry_type.is_dir() || entry_type.is_symlink())
                     {
                         return ignore::WalkState::Continue;


Test patch:
diff --git a/tests/testenv/mod.rs b/tests/testenv/mod.rs
index 886b095..3aec1a4 100644
--- a/tests/testenv/mod.rs
+++ b/tests/testenv/mod.rs
@@ -1,16 +1,12 @@
-use std;
 use std::env;
 use std::fs;
-use std::io;
-use std::io::Write;
-use std::path::{Path, PathBuf};
-use std::process;
-
+use std::io::{self, Write};
 #[cfg(unix)]
 use std::os::unix;
-
 #[cfg(windows)]
 use std::os::windows;
+use std::path::{Path, PathBuf};
+use std::process;
 
 use tempdir::TempDir;
 
diff --git a/tests/tests.rs b/tests/tests.rs
index be21494..4420561 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -1,14 +1,14 @@
-//! Integration tests for the CLI interface of fd.
-
 mod testenv;
 
-use crate::testenv::TestEnv;
-use regex::escape;
 use std::fs;
 use std::io::Write;
 use std::path::Path;
 use std::time::{Duration, SystemTime};
 
+use regex::escape;
+
+use crate::testenv::TestEnv;
+
 static DEFAULT_DIRS: &'static [&'static str] = &["one/two/three", "one/two/three/directory_foo"];
 
 static DEFAULT_FILES: &'static [&'static str] = &[
@@ -1064,28 +1064,11 @@ fn test_excludes() {
 /// Shell script execution (--exec)
 #[test]
 fn test_exec() {
-    assert_exec_output("--exec");
-}
-
-/// Shell script execution using -exec
-#[test]
-fn test_exec_substitution() {
-    assert_exec_output("-exec");
-}
-
-// Shell script execution using -x
-#[test]
-fn test_exec_short_arg() {
-    assert_exec_output("-x");
-}
-
-#[cfg(test)]
-fn assert_exec_output(exec_style: &str) {
     let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
     // TODO Windows tests: D:file.txt \file.txt \\server\share\file.txt ...
     if !cfg!(windows) {
         te.assert_output(
-            &["--absolute-path", "foo", exec_style, "echo"],
+            &["--absolute-path", "foo", "--exec", "echo"],
             &format!(
                 "{abs_path}/a.foo
                 {abs_path}/one/b.foo
@@ -1098,7 +1081,7 @@ fn assert_exec_output(exec_style: &str) {
         );
 
         te.assert_output(
-            &["foo", exec_style, "echo", "{}"],
+            &["foo", "--exec", "echo", "{}"],
             "a.foo
             one/b.foo
             one/two/C.Foo2
@@ -1108,7 +1091,7 @@ fn assert_exec_output(exec_style: &str) {
         );
 
         te.assert_output(
-            &["foo", exec_style, "echo", "{.}"],
+            &["foo", "--exec", "echo", "{.}"],
             "a
             one/b
             one/two/C
@@ -1118,7 +1101,7 @@ fn assert_exec_output(exec_style: &str) {
         );
 
         te.assert_output(
-            &["foo", exec_style, "echo", "{/}"],
+            &["foo", "--exec", "echo", "{/}"],
             "a.foo
             b.foo
             C.Foo2
@@ -1128,7 +1111,7 @@ fn assert_exec_output(exec_style: &str) {
         );
 
         te.assert_output(
-            &["foo", exec_style, "echo", "{/.}"],
+            &["foo", "--exec", "echo", "{/.}"],
             "a
             b
             C
@@ -1138,7 +1121,7 @@ fn assert_exec_output(exec_style: &str) {
         );
 
         te.assert_output(
-            &["foo", exec_style, "echo", "{//}"],
+            &["foo", "--exec", "echo", "{//}"],
             ".
             one
             one/two
@@ -1147,29 +1130,19 @@ fn assert_exec_output(exec_style: &str) {
             one/two/three",
         );
 
-        te.assert_output(&["e1", exec_style, "printf", "%s.%s\n"], "e1 e2.");
+        te.assert_output(&["e1", "--exec", "printf", "%s.%s\n"], "e1 e2.");
     }
 }
 
 #[test]
 fn test_exec_batch() {
-    assert_exec_batch_output("--exec-batch");
-}
-
-#[test]
-fn test_exec_batch_short_arg() {
-    assert_exec_batch_output("-X");
-}
-
-#[cfg(test)]
-fn assert_exec_batch_output(exec_style: &str) {
     let (te, abs_path) = get_test_env_with_abs_path(DEFAULT_DIRS, DEFAULT_FILES);
     let te = te.normalize_line(true);
 
     // TODO Test for windows
     if !cfg!(windows) {
         te.assert_output(
-            &["--absolute-path", "foo", exec_style, "echo"],
+            &["--absolute-path", "foo", "--exec-batch", "echo"],
             &format!(
                 "{abs_path}/a.foo {abs_path}/one/b.foo {abs_path}/one/two/C.Foo2 {abs_path}/one/two/c.foo {abs_path}/one/two/three/d.foo {abs_path}/one/two/three/directory_foo",
                 abs_path = &abs_path
@@ -1177,34 +1150,37 @@ fn assert_exec_batch_output(exec_style: &str) {
         );
 
         te.assert_output(
-            &["foo", exec_style, "echo", "{}"],
+            &["foo", "--exec-batch", "echo", "{}"],
             "a.foo one/b.foo one/two/C.Foo2 one/two/c.foo one/two/three/d.foo one/two/three/directory_foo",
         );
 
         te.assert_output(
-            &["foo", exec_style, "echo", "{/}"],
+            &["foo", "--exec-batch", "echo", "{/}"],
             "a.foo b.foo C.Foo2 c.foo d.foo directory_foo",
         );
 
-        te.assert_output(&["no_match", exec_style, "echo", "Matched: ", "{/}"], "");
+        te.assert_output(
+            &["no_match", "--exec-batch", "echo", "Matched: ", "{/}"],
+            "",
+        );
 
         te.assert_error(
-            &["foo", exec_style, "echo", "{}", "{}"],
+            &["foo", "--exec-batch", "echo", "{}", "{}"],
             "[fd error]: Only one placeholder allowed for batch commands",
         );
 
         te.assert_error(
-            &["foo", exec_style, "echo", "{/}", ";", "-x", "echo"],
+            &["foo", "--exec-batch", "echo", "{/}", ";", "-x", "echo"],
             "error: The argument '--exec <cmd>' cannot be used with '--exec-batch <cmd>'",
         );
 
         te.assert_error(
-            &["foo", exec_style],
+            &["foo", "--exec-batch"],
             "error: The argument '--exec-batch <cmd>' requires a value but none was supplied",
         );
 
         te.assert_error(
-            &["foo", exec_style, "echo {}"],
+            &["foo", "--exec-batch", "echo {}"],
             "[fd error]: First argument of exec-batch is expected to be a fixed executable",
         );
     }