From beba02757ee2d2341dd7412273c1fe358edd9559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Orvar=20Segerstr=C3=B6m?= Date: Thu, 23 Oct 2014 17:41:05 +0200 Subject: [PATCH] MV implementation Support for all commandline options except one, see src/mv/mv.rs Many tests, included a todo list of more tests too, see tests/mv.rs --- src/mv/mv.rs | 362 ++++++++++++++++++++++++++++++++++++------ test/mv.rs | 432 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 728 insertions(+), 66 deletions(-) diff --git a/src/mv/mv.rs b/src/mv/mv.rs index bb5afc60d..109ea849a 100644 --- a/src/mv/mv.rs +++ b/src/mv/mv.rs @@ -3,87 +3,347 @@ /* * This file is part of the uutils coreutils package. * + * (c) Orvar Segerström * (c) Sokovikov Evgeniy * * For the full copyright and license information, please view the LICENSE file * that was distributed with this source code. */ -#![feature(macro_rules)] +#![feature(macro_rules, if_let)] extern crate getopts; -use std::io::fs; - +use std::io::{BufferedReader, IoResult, fs}; +use std::io::stdio::stdin_raw; +use std::io::fs::PathExtensions; +use std::path::GenericPath; use getopts::{ getopts, optflag, + optflagopt, + optopt, usage, }; -static NAME: &'static str = "mv"; -static VERSION: &'static str = "0.0.1"; - #[path = "../common/util.rs"] mod util; -pub fn uumain(args: Vec) -> int { - let opts = [ - optflag("h", "help", "display this help and exit"), - optflag("V", "version", "output version information and exit"), - ]; - let matches = match getopts(args.tail(), opts) { - Ok(m) => m, - Err(f) => crash!(1, "Invalid options\n{}", f) - }; +static NAME: &'static str = "mv"; +static VERSION: &'static str = "0.0.1"; - let progname = &args[0]; - let usage = usage("Move SOURCE to DEST", opts); - - if matches.opt_present("version") { - println!("{}", VERSION); - return 0; - } - - if matches.opt_present("help") { - help(progname.as_slice(), usage.as_slice()); - return 0; - } - - let source = if matches.free.len() < 1 { - println!("error: Missing SOURCE argument. Try --help."); - return 1; - } else { - Path::new(matches.free[0].as_slice()) - }; - - let dest = if matches.free.len() < 2 { - println!("error: Missing DEST argument. Try --help."); - return 1; - } else { - Path::new(matches.free[1].as_slice()) - }; - - mv(source, dest) +pub struct Behaviour { + overwrite: OverwriteMode, + backup: BackupMode, + suffix: String, + update: bool, + target_dir: Option, + no_target_dir: bool, + verbose: bool, } -fn mv(source: Path, dest: Path) -> int { - let io_result = fs::rename(&source, &dest); +#[deriving(Eq, PartialEq)] +pub enum OverwriteMode { + NoClobber, + Interactive, + Force, +} - if io_result.is_err() { - let err = io_result.unwrap_err(); - println!("error: {:s}", err.to_string()); - 1 +#[deriving(Eq, PartialEq)] +pub enum BackupMode { + NoBackup, + SimpleBackup, + NumberedBackup, + ExistingBackup, +} + +pub fn uumain(args: Vec) -> int { + let program = args[0].as_slice(); + let opts = [ + optflagopt("", "backup", "make a backup of each existing destination file", "CONTROL"), + optflag("b", "", "like --backup but does not accept an argument"), + optflag("f", "force", "do not prompt before overwriting"), + optflag("i", "interactive", "prompt before override"), + optflag("n", "no-clobber", "do not overwrite an existing file"), + // I have yet to find a use-case (and thereby write a test) where this option is useful. + //optflag("", "strip-trailing-slashes", "remove any trailing slashes from each SOURCE\n \ + // argument"), + optopt("S", "suffix", "override the usual backup suffix", "SUFFIX"), + optopt("t", "target-directory", "move all SOURCE arguments into DIRECTORY", "DIRECTORY"), + optflag("T", "no-target-directory", "treat DEST as a normal file"), + optflag("u", "update", "move only when the SOURCE file is newer\n \ + than the destination file or when the\n \ + destination file is missing"), + optflag("v", "verbose", "explain what is being done"), + optflag("h", "help", "display this help and exit"), + optflag("V", "version", "output version information and exit"), + ]; + + let matches = match getopts(args.tail(), opts) { + Ok(m) => m, + Err(f) => { + show_error!("Invalid options\n{}", f); + return 1; + } + }; + let usage = usage("Move SOURCE to DEST, or multiple SOURCE(s) to DIRECTORY.", opts); + + /* This does not exactly match the GNU implementation: + * The GNU mv defaults to Force, but if more than one of the + * overwrite options are supplied, only the last takes effect. + * To default to no-clobber in that situation seems safer: + */ + let overwrite_mode = if matches.opt_present("no-clobber") { + NoClobber + } else if matches.opt_present("interactive") { + Interactive } else { - 0 + Force + }; + + let backup_mode = if matches.opt_present("b") { + SimpleBackup + } else if matches.opt_present("backup") { + match matches.opt_str("backup") { + None => SimpleBackup, + Some(mode) => match mode.as_slice() { + "simple" | "never" => SimpleBackup, + "numbered" | "t" => NumberedBackup, + "existing" | "nil" => ExistingBackup, + "none" | "off" => NoBackup, + x => { + show_error!("invalid argument ‘{}’ for ‘backup type’\n\ + Try 'mv --help' for more information.", x); + return 1; + } + } + } + } else { + NoBackup + }; + + if overwrite_mode == NoClobber && backup_mode != NoBackup { + show_error!("options --backup and --no-clobber are mutually exclusive\n\ + Try 'mv --help' for more information."); + return 1; } + + let backup_suffix = if matches.opt_present("suffix") { + match matches.opt_str("suffix") { + Some(x) => x, + None => { + show_error!("option '--suffix' requires an argument\n\ + Try 'mv --help' for more information."); + return 1; + } + } + } else { + "~".into_string() + }; + + if matches.opt_present("T") && matches.opt_present("t") { + show_error!("cannot combine --target-directory (-t) and --no-target-directory (-T)"); + return 1; + } + + let behaviour = Behaviour { + overwrite: overwrite_mode, + backup: backup_mode, + suffix: backup_suffix, + update: matches.opt_present("u"), + target_dir: matches.opt_str("t"), + no_target_dir: matches.opt_present("T"), + verbose: matches.opt_present("v"), + }; + + let string_to_path = |s: &String| { Path::new(s.as_slice()) }; + let paths: Vec = matches.free.iter().map(string_to_path).collect(); + + if matches.opt_present("version") { + version(); + 0 + } else if matches.opt_present("help") { + help(program.as_slice(), usage.as_slice()); + 0 + } else { + exec(paths.as_slice(), behaviour) + } +} + +fn version() { + println!("{} {}", NAME, VERSION); } fn help(progname: &str, usage: &str) { let msg = format!("Usage: {0} SOURCE DEST\n \ - or: {0} SOURCE... DIRECTORY\n \ + or: {0} SOURCE... DIRECTORY \ \n\ {1}", progname, usage); println!("{}", msg); } +fn exec(files: &[Path], b: Behaviour) -> int { + match b.target_dir { + Some(ref name) => return move_files_into_dir(files, &Path::new(name.as_slice()), &b), + None => {} + } + match files { + [] | [_] => { + show_error!("missing file operand\n\ + Try 'mv --help' for more information."); + return 1; + }, + [ref source, ref target] => { + if !source.exists() { + show_error!("cannot stat ‘{}’: No such file or directory", source.display()); + return 1; + } + + if target.is_dir() { + if b.no_target_dir { + if !source.is_dir() { + show_error!("cannot overwrite directory ‘{}’ with non-directory", + target.display()); + return 1; + } + + return match rename(source, target, &b) { + Err(e) => { + show_error!("{}", e); + 1 + }, + _ => 0 + } + } + + return move_files_into_dir(&[source.clone()], target, &b); + } + + match rename(source, target, &b) { + Err(e) => { + show_error!("{}", e); + return 1; + }, + _ => {} + } + } + fs => { + if b.no_target_dir { + show_error!("mv: extra operand ‘{}’\n\ + Try 'mv --help' for more information.", fs[2].display()); + return 1; + } + let target_dir = fs.last().unwrap(); + move_files_into_dir(fs.init(), target_dir, &b); + } + } + 0 +} + +fn move_files_into_dir(files: &[Path], target_dir: &Path, b: &Behaviour) -> int { + if !target_dir.is_dir() { + show_error!("target ‘{}’ is not a directory", target_dir.display()); + return 1; + } + + let mut all_successful = true; + for sourcepath in files.iter() { + let targetpath = match sourcepath.filename_str() { + Some(name) => target_dir.join(name), + None => { + show_error!("cannot stat ‘{}’: No such file or directory", + sourcepath.display()); + + all_successful = false; + continue; + } + }; + + match rename(sourcepath, &targetpath, b) { + Err(e) => { + show_error!("mv: cannot move ‘{}’ to ‘{}’: {}", + sourcepath.display(), targetpath.display(), e); + all_successful = false; + }, + _ => {} + } + }; + if all_successful { 0 } else { 1 } +} + +fn rename(from: &Path, to: &Path, b: &Behaviour) -> IoResult<()> { + let mut backup_path = None; + + if to.is_file() { + match b.overwrite { + NoClobber => return Ok(()), + Interactive => { + print!("{}: overwrite ‘{}’? ", NAME, to.display()); + if !read_yes() { + return Ok(()); + } + }, + Force => {} + }; + + backup_path = match b.backup { + NoBackup => None, + SimpleBackup => Some(simple_backup_path(to, &b.suffix)), + NumberedBackup => Some(numbered_backup_path(to)), + ExistingBackup => Some(existing_backup_path(to, &b.suffix)) + }; + if let Some(ref p) = backup_path { + try!(fs::rename(to, p)); + } + + if b.update { + if try!(from.stat()).modified <= try!(to.stat()).modified { + return Ok(()); + } + } + } + + if b.verbose { + print!("‘{}’ -> ‘{}’", from.display(), to.display()); + match backup_path { + Some(path) => println!(" (backup: ‘{}’)", path.display()), + None => println!("") + } + } + fs::rename(from, to) +} + +fn read_yes() -> bool { + match BufferedReader::new(stdin_raw()).read_line() { + Ok(s) => match s.as_slice().slice_shift_char() { + (Some(x), _) => x == 'y' || x == 'Y', + _ => false + }, + _ => false + } +} + +fn simple_backup_path(path: &Path, suffix: &String) -> Path { + let mut p = path.clone().into_vec(); + p.push_all(suffix.as_slice().as_bytes()); + return Path::new(p); +} + +fn numbered_backup_path(path: &Path) -> Path { + let mut i: u64 = 1; + loop { + let new_path = simple_backup_path(path, &format!(".~{}~", i)); + if !new_path.exists() { + return new_path; + } + i = i + 1; + } +} + +fn existing_backup_path(path: &Path, suffix: &String) -> Path { + let test_path = simple_backup_path(path, &".~1~".into_string()); + if test_path.exists() { + return numbered_backup_path(path); + } + return simple_backup_path(path, suffix); +} diff --git a/test/mv.rs b/test/mv.rs index 13d380abd..143e50b92 100644 --- a/test/mv.rs +++ b/test/mv.rs @@ -1,24 +1,426 @@ +#![feature(macro_rules)] + +extern crate time; + +use std::io::{process, fs, FilePermission}; use std::io::process::Command; -use std::io::fs::{PathExtensions}; +use std::io::fs::PathExtensions; +use std::io::pipe::PipeStream; +use std::str::from_utf8; static EXE: &'static str = "./mv"; -static TEST_HELLO_WORLD_SOURCE: &'static str = "hello_world.txt"; -static TEST_HELLO_WORLD_DEST: &'static str = "move_of_hello_world.txt"; -#[test] -fn test_mv() { - let prog = Command::new(EXE) - .arg(TEST_HELLO_WORLD_SOURCE) - .arg(TEST_HELLO_WORLD_DEST) - .status(); - let exit_success = prog.unwrap().success(); - assert_eq!(exit_success, true); +macro_rules! assert_empty_stderr( + ($cond:expr) => ( + if $cond.stderr.len() > 0 { + fail!(format!("stderr: {}", $cond.stderr)) + } + ); +) +struct CmdResult { + success: bool, + stderr: String, + stdout: String, +} +fn run(cmd: &mut Command) -> CmdResult { + let prog = cmd.spawn().unwrap().wait_with_output().unwrap(); + CmdResult { + success: prog.status.success(), + stderr: from_utf8(prog.error.as_slice()).unwrap().into_string(), + stdout: from_utf8(prog.output.as_slice()).unwrap().into_string(), + } +} +fn run_interactive(cmd: &mut Command, f: |&mut PipeStream|) -> CmdResult { + let stdin_cfg = process::CreatePipe(true, false); + let mut command = cmd.stdin(stdin_cfg).spawn().unwrap(); - let dest = Path::new(TEST_HELLO_WORLD_DEST); - assert!(dest.exists() == true); + f(command.stdin.as_mut().unwrap()); - let source = Path::new(TEST_HELLO_WORLD_SOURCE); - assert!(source.exists() == false); + let prog = command.wait_with_output().unwrap(); + CmdResult { + success: prog.status.success(), + stderr: from_utf8(prog.error.as_slice()).unwrap().into_string(), + stdout: from_utf8(prog.output.as_slice()).unwrap().into_string(), + } } +fn mkdir(dir: &str) { + fs::mkdir(&Path::new(dir), FilePermission::from_bits_truncate(0o755 as u32)).unwrap(); +} +fn touch(file: &str) { + fs::File::create(&Path::new(file)).unwrap(); +} + + +#[test] +fn test_mv_rename_dir() { + let dir1 = "test_mv_rename_dir"; + let dir2 = "test_mv_rename_dir2"; + + mkdir(dir1); + + let result = run(Command::new(EXE).arg(dir1).arg(dir2)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(Path::new(dir2).is_dir()); +} + +#[test] +fn test_mv_rename_file() { + let file1 = "test_mv_rename_file"; + let file2 = "test_mv_rename_file2"; + + touch(file1); + + let result = run(Command::new(EXE).arg(file1).arg(file2)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(Path::new(file2).is_file()); +} + +#[test] +fn test_mv_move_file_into_dir() { + let dir = "test_mv_move_file_into_dir_dir"; + let file = "test_mv_move_file_into_dir_file"; + + mkdir(dir); + touch(file); + + let result = run(Command::new(EXE).arg(file).arg(dir)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(Path::new(format!("{}/{}", dir, file)).is_file()); +} + +#[test] +fn test_mv_multiple_files() { + let target_dir = "test_mv_multiple_files_dir"; + let file_a = "test_mv_multiple_file_a"; + let file_b = "test_mv_multiple_file_b"; + + mkdir(target_dir); + touch(file_a); + touch(file_b); + + let result = run(Command::new(EXE).arg(file_a).arg(file_b).arg(target_dir)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(Path::new(format!("{}/{}", target_dir, file_a)).is_file()); + assert!(Path::new(format!("{}/{}", target_dir, file_b)).is_file()); +} + +#[test] +fn test_mv_multiple_folders() { + let target_dir = "test_mv_multiple_dirs_dir"; + let dir_a = "test_mv_multiple_dir_a"; + let dir_b = "test_mv_multiple_dir_b"; + + mkdir(target_dir); + mkdir(dir_a); + mkdir(dir_b); + + let result = run(Command::new(EXE).arg(dir_a).arg(dir_b).arg(target_dir)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(Path::new(format!("{}/{}", target_dir, dir_a)).is_dir()); + assert!(Path::new(format!("{}/{}", target_dir, dir_b)).is_dir()); +} + +#[test] +fn test_mv_interactive() { + let file_a = "test_mv_interactive_file_a"; + let file_b = "test_mv_interactive_file_b"; + + touch(file_a); + touch(file_b); + + + let ia1 = |stdin: &mut PipeStream| { + stdin.write(b"n").unwrap(); + }; + let result1 = run_interactive(Command::new(EXE).arg("-i").arg(file_a).arg(file_b), ia1); + + assert_empty_stderr!(result1); + assert!(result1.success); + + assert!(Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); + + + let ia2 = |stdin: &mut PipeStream| { + stdin.write(b"Yesh").unwrap(); + }; + let result2 = run_interactive(Command::new(EXE).arg("-i").arg(file_a).arg(file_b), ia2); + + assert_empty_stderr!(result2); + assert!(result2.success); + + assert!(!Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); +} + +#[test] +fn test_mv_no_clobber() { + let file_a = "test_mv_no_clobber_file_a"; + let file_b = "test_mv_no_clobber_file_b"; + + touch(file_a); + touch(file_b); + + let result = run(Command::new(EXE).arg("-n").arg(file_a).arg(file_b)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); +} + +#[test] +fn test_mv_replace_file() { + let file_a = "test_mv_replace_file_a"; + let file_b = "test_mv_replace_file_b"; + + touch(file_a); + touch(file_b); + + let result = run(Command::new(EXE).arg(file_a).arg(file_b)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); +} + +#[test] +fn test_mv_force_replace_file() { + let file_a = "test_mv_force_replace_file_a"; + let file_b = "test_mv_force_replace_file_b"; + + touch(file_a); + touch(file_b); + + let result = run(Command::new(EXE).arg("--force").arg(file_a).arg(file_b)); + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); +} + +#[test] +fn test_mv_simple_backup() { + let file_a = "test_mv_simple_backup_file_a"; + let file_b = "test_mv_simple_backup_file_b"; + + touch(file_a); + touch(file_b); + let result = run(Command::new(EXE).arg("-b").arg(file_a).arg(file_b)); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); + assert!(Path::new(format!("{}~", file_b)).is_file()); +} + +#[test] +fn test_mv_custom_backup_suffix() { + let file_a = "test_mv_custom_backup_suffix_file_a"; + let file_b = "test_mv_custom_backup_suffix_file_b"; + let suffix = "super-suffix-of-the-century"; + + touch(file_a); + touch(file_b); + let result = run(Command::new(EXE) + .arg("-b").arg(format!("--suffix={}", suffix)) + .arg(file_a).arg(file_b)); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); + assert!(Path::new(format!("{}{}", file_b, suffix)).is_file()); +} + +#[test] +fn test_mv_backup_numbering() { + let file_a = "test_mv_backup_numbering_file_a"; + let file_b = "test_mv_backup_numbering_file_b"; + + touch(file_a); + touch(file_b); + let result = run(Command::new(EXE).arg("--backup=t").arg(file_a).arg(file_b)); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); + assert!(Path::new(format!("{}.~1~", file_b)).is_file()); +} + +#[test] +fn test_mv_existing_backup() { + let file_a = "test_mv_existing_backup_file_a"; + let file_b = "test_mv_existing_backup_file_b"; + let file_b_backup = "test_mv_existing_backup_file_b.~1~"; + let resulting_backup = "test_mv_existing_backup_file_b.~2~"; + + touch(file_a); + touch(file_b); + touch(file_b_backup); + let result = run(Command::new(EXE).arg("--backup=nil").arg(file_a).arg(file_b)); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); + assert!(Path::new(file_b_backup).is_file()); + assert!(Path::new(resulting_backup).is_file()); +} + +#[test] +fn test_mv_update_option() { + let file_a = "test_mv_update_option_file_a"; + let file_b = "test_mv_update_option_file_b"; + + touch(file_a); + touch(file_b); + let now = (time::get_time().sec * 1000) as u64; + fs::change_file_times(&Path::new(file_a), now, now).unwrap(); + fs::change_file_times(&Path::new(file_b), now, now+3600).unwrap(); + + let result1 = run(Command::new(EXE).arg("--update").arg(file_a).arg(file_b)); + + assert_empty_stderr!(result1); + assert!(result1.success); + + assert!(Path::new(file_a).is_file()); + assert!(Path::new(file_b).is_file()); + + let result2 = run(Command::new(EXE).arg("--update").arg(file_b).arg(file_a)); + + assert_empty_stderr!(result2); + assert!(result2.success); + + assert!(Path::new(file_a).is_file()); + assert!(!Path::new(file_b).is_file()); +} + +#[test] +fn test_mv_target_dir() { + let dir = "test_mv_target_dir_dir"; + let file_a = "test_mv_target_dir_file_a"; + let file_b = "test_mv_target_dir_file_b"; + + touch(file_a); + touch(file_b); + mkdir(dir); + let result = run(Command::new(EXE).arg("-t").arg(dir).arg(file_a).arg(file_b)); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(file_a).is_file()); + assert!(!Path::new(file_b).is_file()); + assert!(Path::new(format!("{}/{}", dir, file_a)).is_file()); + assert!(Path::new(format!("{}/{}", dir, file_b)).is_file()); +} + +#[test] +fn test_mv_overwrite_dir() { + let dir_a = "test_mv_overwrite_dir_a"; + let dir_b = "test_mv_overwrite_dir_b"; + + mkdir(dir_a); + mkdir(dir_b); + let result = run(Command::new(EXE).arg("-T").arg(dir_a).arg(dir_b)); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(!Path::new(dir_a).is_dir()); + assert!(Path::new(dir_b).is_dir()); +} + +#[test] +fn test_mv_errors() { + let dir = "test_mv_errors_dir"; + let file_a = "test_mv_errors_file_a"; + let file_b = "test_mv_errors_file_b"; + mkdir(dir); + touch(file_a); + touch(file_b); + + // $ mv -T -t a b + // mv: cannot combine --target-directory (-t) and --no-target-directory (-T) + let result = run(Command::new(EXE).arg("-T").arg("-t").arg(dir).arg(file_a).arg(file_b)); + assert_eq!(result.stderr.as_slice(), + "mv: error: cannot combine --target-directory (-t) and --no-target-directory (-T)\n"); + assert!(!result.success); + + + // $ touch file && mkdir dir + // $ mv -T file dir + // err == mv: cannot overwrite directory ‘dir’ with non-directory + let result = run(Command::new(EXE).arg("-T").arg(file_a).arg(dir)); + assert_eq!(result.stderr.as_slice(), + format!("mv: error: cannot overwrite directory ‘{}’ with non-directory\n", dir).as_slice()); + assert!(!result.success); + + // $ mkdir dir && touch file + // $ mv dir file + // err == mv: cannot overwrite non-directory ‘file’ with directory ‘dir’ + let result = run(Command::new(EXE).arg(dir).arg(file_a)); + assert!(result.stderr.len() > 0); + assert!(!result.success); +} + +#[test] +fn test_mv_verbose() { + let dir = "test_mv_verbose_dir"; + let file_a = "test_mv_verbose_file_a"; + let file_b = "test_mv_verbose_file_b"; + mkdir(dir); + touch(file_a); + touch(file_b); + + let result = run(Command::new(EXE).arg("-v").arg(file_a).arg(file_b)); + assert_empty_stderr!(result); + assert_eq!(result.stdout.as_slice(), + format!("‘{}’ -> ‘{}’\n", file_a, file_b).as_slice()); + assert!(result.success); + + + touch(file_a); + let result = run(Command::new(EXE).arg("-vb").arg(file_a).arg(file_b)); + assert_empty_stderr!(result); + assert_eq!(result.stdout.as_slice(), + format!("‘{}’ -> ‘{}’ (backup: ‘{}~’)\n", file_a, file_b, file_b).as_slice()); + assert!(result.success); +} + + +// Todo: + +// $ touch a b +// $ chmod -w b +// $ ll +// total 0 +// -rw-rw-r-- 1 user user 0 okt 25 11:21 a +// -r--r--r-- 1 user user 0 okt 25 11:21 b +// $ +// $ mv -v a b +// mv: try to overwrite ‘b’, overriding mode 0444 (r--r--r--)? y +// ‘a’ -> ‘b’ + +