diff --git a/prebuilt-x86-64-linux/lndups b/prebuilt-x86-64-linux/lndups
index 116470a..b85d5eb 100755
Binary files a/prebuilt-x86-64-linux/lndups and b/prebuilt-x86-64-linux/lndups differ
diff --git a/src/main.rs b/src/main.rs
index 4109f7e..92975ec 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 extern crate shlex;
 extern crate structopt;
-use std::io::{Read,Write};
+use std::io::{Read, Write, BufReader, BufRead};
 use std::path::{Path,PathBuf};
 use std::collections::HashMap;
 use std::os::linux::fs::MetadataExt as MetadataExtLinux;
@@ -46,15 +46,23 @@ struct CLIArguments {
     dry_run: bool,
 
     #[structopt(short="i",
-                help="Prompt once before operating")]
+                help="Prompt once before operating\nNever occurs if no targets are provided")]
     prompt: bool,
 
     #[structopt(short, long, value_name="VALUE",
                 help="Minimum file size to be considered for hardlinking\nNever goes below 1 (the default)")]
     min_size: Option<u64>,
 
+    #[structopt(short, long, value_name="SEPARATOR",
+                help="Separator between sets of targets (default: ';')")]
+    sep: Option<String>,
+
+    #[structopt(short, long, value_name="FILE",
+                help="File to source arguments from (can be '-' for stdin)")]
+    argument_file: Option<String>,
+
     #[structopt(value_name="TARGET",
-                help="Target files and directories (recursive)\nEach ';' denotes a new set of targets\n  Each set of targets are separate from all other sets\n  All targets must be on the same device\nAll symlinks are ignored")]
+                help="Target files and directories (recursive)\nEach SEPARATOR denotes a new set of targets\n  Each set of targets are separate from all other sets\n  All targets must be on the same device\nAll symlinks are ignored\n'-' is not treated as special")]
     targets: Vec<String>,
 }
 
@@ -78,14 +86,48 @@ fn prompt_confirm(run_targets: &Vec<Vec<String>>) -> bool {
     response.to_lowercase().starts_with("y")
 }
 
-fn process_args() -> (Vec<Vec<PathBuf>>, Config) {
-    let args = CLIArguments::from_args();
+fn read_argument_file(arg_file: &Path, dest: &mut Vec<String>) -> Result<(), String> {
+    if !arg_file.is_file() {
+        return Err(format!("File does not exist or is not a normal file ({})", shlex::quote(&arg_file.to_string_lossy())));
+    }
+    if let Ok(f) = std::fs::File::open(arg_file) {
+        let reader = BufReader::new(f);
+        for line in reader.lines() {
+            match line {
+                Ok(line) => dest.push(line),
+                Err(err) => return Err(format!("Error reading line: {}", err))
+            }
+        }
+        Ok(())
+    } else {
+        Err(format!("Could not open {}", shlex::quote(&arg_file.to_string_lossy())))
+    }
+}
 
-    let run_targets: Vec<Vec<String>> = {
-        let mut rt = split_vec(&args.targets, ";");
-        rt.retain(|targets| !targets.is_empty() );
-        rt
-    };
+fn process_args() -> (Vec<Vec<PathBuf>>, Config) {
+    let mut args = CLIArguments::from_args();
+    let verbosity = args.verbose - args.quiet;
+
+    if let Some(arg_file) = args.argument_file {
+        if !args.targets.is_empty() {
+            eprintln!("No targets should be provided as cli arguments if arguments are being read from file");
+            std::process::exit(1);
+        }
+        let path = Path::new(&arg_file);
+        if let Err(s) = read_argument_file(path, &mut args.targets) {
+            eprintln!("Error reading argument file: {}", s);
+            std::process::exit(1);
+        }
+    }
+
+    let run_targets: Vec<Vec<String>> = split_vec(&args.targets, &args.sep.unwrap_or(";".to_string()));
+
+    if run_targets.is_empty() {
+        if verbosity > 0 {
+            println!("No targets provided");
+            std::process::exit(0);
+        }
+    }
 
     if args.prompt {
         if !prompt_confirm(&run_targets) {
@@ -106,39 +148,49 @@ fn process_args() -> (Vec<Vec<PathBuf>>, Config) {
 
 
     for paths in &run_paths {
-        assert_all_same_device(paths);
+        if let Err(s) = assert_all_same_device(paths) {
+            eprintln!("{}", s);
+            std::process::exit(1);
+        }
     }
 
     (run_paths, Config {
         min_size: args.min_size.unwrap_or(1),
         no_brace_output: args.no_brace_output,
         dry_run: args.dry_run,
-        verbosity: args.verbose - args.quiet
+        verbosity
     })
 }
 
 /// exit on error
-fn get_st_dev(file: &PathBuf) -> u64 {
+fn get_st_dev(file: &PathBuf) -> Result<u64, String> {
     if let Ok(metadata) = std::fs::metadata(file) {
-        metadata.st_dev()
+        Ok(metadata.st_dev())
     } else {
-        eprintln!("Failed to retrive device id for {}", shlex::quote(&file.to_string_lossy()));
-        std::process::exit(1);
+        Err(format!("Failed to retrive device id for {}", shlex::quote(&file.to_string_lossy())))
     }
 }
 
 /// minimum length of slice = 2
-fn assert_all_same_device(paths: &[PathBuf]) {
+fn assert_all_same_device(paths: &[PathBuf]) -> Result<(), String> {
     if paths.len() <= 1 {
-        return
+        return Ok(())
     }
-    let first_device_id = get_st_dev(&paths[0]);
-    let wrong: Vec<&PathBuf> = paths[1..].iter().filter(|path| get_st_dev(path) == first_device_id).collect();
-    if !wrong.is_empty() {
-        for path in wrong {
-            eprintln!("Device ids must all be the same; got different for: {}", shlex::quote(&path.to_string_lossy()));
+    let first_device_id = get_st_dev(&paths[0])?;
+    let mut wrong: Vec<&PathBuf> = Vec::new();
+    for path in &paths[1..] {
+        if get_st_dev(path)? != first_device_id {
+            wrong.push(path);
         }
-        std::process::exit(1);
+    }
+    if wrong.is_empty() {
+        Ok(())
+    } else {
+        let mut s = String::new();
+        for path in wrong {
+            s.push_str(&format!("Device ids must all be the same; got different for: {}", shlex::quote(&path.to_string_lossy())));
+        }
+        Err(s)
     }
 }
 
@@ -329,7 +381,7 @@ fn common_suffix<'a>(s1: &'a str, s2: &'a str) -> &'a str {
     &s1[s1.len() - len..]
 }
 
-fn split_vec(input: &[String], delimiter: &str) -> Vec<Vec<String>> {
+fn split_vec(input: &[String], delimiter: &String) -> Vec<Vec<String>> {
     let mut result: Vec<Vec<String>> = Vec::new();
     let mut current_vec: Vec<String> = Vec::new();
     for item in input.iter() {