added argument file option; added option to change separator; changed error management
This commit is contained in:
parent
6ae68ea5e6
commit
f0b4b237bb
2 changed files with 77 additions and 25 deletions
Binary file not shown.
102
src/main.rs
102
src/main.rs
|
@ -1,6 +1,6 @@
|
||||||
extern crate shlex;
|
extern crate shlex;
|
||||||
extern crate structopt;
|
extern crate structopt;
|
||||||
use std::io::{Read,Write};
|
use std::io::{Read, Write, BufReader, BufRead};
|
||||||
use std::path::{Path,PathBuf};
|
use std::path::{Path,PathBuf};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::os::linux::fs::MetadataExt as MetadataExtLinux;
|
use std::os::linux::fs::MetadataExt as MetadataExtLinux;
|
||||||
|
@ -46,15 +46,23 @@ struct CLIArguments {
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
|
||||||
#[structopt(short="i",
|
#[structopt(short="i",
|
||||||
help="Prompt once before operating")]
|
help="Prompt once before operating\nNever occurs if no targets are provided")]
|
||||||
prompt: bool,
|
prompt: bool,
|
||||||
|
|
||||||
#[structopt(short, long, value_name="VALUE",
|
#[structopt(short, long, value_name="VALUE",
|
||||||
help="Minimum file size to be considered for hardlinking\nNever goes below 1 (the default)")]
|
help="Minimum file size to be considered for hardlinking\nNever goes below 1 (the default)")]
|
||||||
min_size: Option<u64>,
|
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",
|
#[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>,
|
targets: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,14 +86,48 @@ fn prompt_confirm(run_targets: &Vec<Vec<String>>) -> bool {
|
||||||
response.to_lowercase().starts_with("y")
|
response.to_lowercase().starts_with("y")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn process_args() -> (Vec<Vec<PathBuf>>, Config) {
|
fn read_argument_file(arg_file: &Path, dest: &mut Vec<String>) -> Result<(), String> {
|
||||||
let args = CLIArguments::from_args();
|
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>> = {
|
fn process_args() -> (Vec<Vec<PathBuf>>, Config) {
|
||||||
let mut rt = split_vec(&args.targets, ";");
|
let mut args = CLIArguments::from_args();
|
||||||
rt.retain(|targets| !targets.is_empty() );
|
let verbosity = args.verbose - args.quiet;
|
||||||
rt
|
|
||||||
};
|
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 args.prompt {
|
||||||
if !prompt_confirm(&run_targets) {
|
if !prompt_confirm(&run_targets) {
|
||||||
|
@ -106,39 +148,49 @@ fn process_args() -> (Vec<Vec<PathBuf>>, Config) {
|
||||||
|
|
||||||
|
|
||||||
for paths in &run_paths {
|
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 {
|
(run_paths, Config {
|
||||||
min_size: args.min_size.unwrap_or(1),
|
min_size: args.min_size.unwrap_or(1),
|
||||||
no_brace_output: args.no_brace_output,
|
no_brace_output: args.no_brace_output,
|
||||||
dry_run: args.dry_run,
|
dry_run: args.dry_run,
|
||||||
verbosity: args.verbose - args.quiet
|
verbosity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// exit on error
|
/// 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) {
|
if let Ok(metadata) = std::fs::metadata(file) {
|
||||||
metadata.st_dev()
|
Ok(metadata.st_dev())
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Failed to retrive device id for {}", shlex::quote(&file.to_string_lossy()));
|
Err(format!("Failed to retrive device id for {}", shlex::quote(&file.to_string_lossy())))
|
||||||
std::process::exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// minimum length of slice = 2
|
/// 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 {
|
if paths.len() <= 1 {
|
||||||
return
|
return Ok(())
|
||||||
}
|
}
|
||||||
let first_device_id = get_st_dev(&paths[0]);
|
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();
|
let mut wrong: Vec<&PathBuf> = Vec::new();
|
||||||
if !wrong.is_empty() {
|
for path in &paths[1..] {
|
||||||
for path in wrong {
|
if get_st_dev(path)? != first_device_id {
|
||||||
eprintln!("Device ids must all be the same; got different for: {}", shlex::quote(&path.to_string_lossy()));
|
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..]
|
&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 result: Vec<Vec<String>> = Vec::new();
|
||||||
let mut current_vec: Vec<String> = Vec::new();
|
let mut current_vec: Vec<String> = Vec::new();
|
||||||
for item in input.iter() {
|
for item in input.iter() {
|
||||||
|
|
Loading…
Add table
Reference in a new issue