1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-08-01 13:37:48 +00:00

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
This commit is contained in:
Orvar Segerström 2014-10-23 17:41:05 +02:00
parent 1f37abcc9c
commit beba02757e
2 changed files with 728 additions and 66 deletions

View file

@ -3,87 +3,347 @@
/* /*
* This file is part of the uutils coreutils package. * This file is part of the uutils coreutils package.
* *
* (c) Orvar Segerström <orvarsegerstrom@gmail.com>
* (c) Sokovikov Evgeniy <skv-headless@yandex.ru> * (c) Sokovikov Evgeniy <skv-headless@yandex.ru>
* *
* For the full copyright and license information, please view the LICENSE file * For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code. * that was distributed with this source code.
*/ */
#![feature(macro_rules)] #![feature(macro_rules, if_let)]
extern crate getopts; 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::{ use getopts::{
getopts, getopts,
optflag, optflag,
optflagopt,
optopt,
usage, usage,
}; };
static NAME: &'static str = "mv";
static VERSION: &'static str = "0.0.1";
#[path = "../common/util.rs"] #[path = "../common/util.rs"]
mod util; mod util;
static NAME: &'static str = "mv";
static VERSION: &'static str = "0.0.1";
pub struct Behaviour {
overwrite: OverwriteMode,
backup: BackupMode,
suffix: String,
update: bool,
target_dir: Option<String>,
no_target_dir: bool,
verbose: bool,
}
#[deriving(Eq, PartialEq)]
pub enum OverwriteMode {
NoClobber,
Interactive,
Force,
}
#[deriving(Eq, PartialEq)]
pub enum BackupMode {
NoBackup,
SimpleBackup,
NumberedBackup,
ExistingBackup,
}
pub fn uumain(args: Vec<String>) -> int { pub fn uumain(args: Vec<String>) -> int {
let program = args[0].as_slice();
let opts = [ 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("h", "help", "display this help and exit"),
optflag("V", "version", "output version information and exit"), optflag("V", "version", "output version information and exit"),
]; ];
let matches = match getopts(args.tail(), opts) { let matches = match getopts(args.tail(), opts) {
Ok(m) => m, Ok(m) => m,
Err(f) => crash!(1, "Invalid options\n{}", f) 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 {
Force
}; };
let progname = &args[0]; let backup_mode = if matches.opt_present("b") {
let usage = usage("Move SOURCE to DEST", opts); 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<Path> = matches.free.iter().map(string_to_path).collect();
if matches.opt_present("version") { if matches.opt_present("version") {
println!("{}", VERSION); version();
return 0; 0
} } else if matches.opt_present("help") {
help(program.as_slice(), usage.as_slice());
if matches.opt_present("help") { 0
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 { } else {
Path::new(matches.free[0].as_slice()) exec(paths.as_slice(), behaviour)
}; }
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)
} }
fn mv(source: Path, dest: Path) -> int { fn version() {
let io_result = fs::rename(&source, &dest); println!("{} {}", NAME, VERSION);
if io_result.is_err() {
let err = io_result.unwrap_err();
println!("error: {:s}", err.to_string());
1
} else {
0
}
} }
fn help(progname: &str, usage: &str) { fn help(progname: &str, usage: &str) {
let msg = format!("Usage: {0} SOURCE DEST\n \ let msg = format!("Usage: {0} SOURCE DEST\n \
or: {0} SOURCE... DIRECTORY\n \ or: {0} SOURCE... DIRECTORY \
\n\ \n\
{1}", progname, usage); {1}", progname, usage);
println!("{}", msg); 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);
}

View file

@ -1,24 +1,426 @@
#![feature(macro_rules)]
extern crate time;
use std::io::{process, fs, FilePermission};
use std::io::process::Command; 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 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(); macro_rules! assert_empty_stderr(
assert_eq!(exit_success, true); ($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); f(command.stdin.as_mut().unwrap());
assert!(dest.exists() == true);
let source = Path::new(TEST_HELLO_WORLD_SOURCE); let prog = command.wait_with_output().unwrap();
assert!(source.exists() == false); 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