diff --git a/Makefile b/Makefile index ff8adc843..7dab6188c 100644 --- a/Makefile +++ b/Makefile @@ -60,6 +60,7 @@ PROGS := \ fold \ link \ hashsum \ + ln \ mkdir \ mv \ nl \ @@ -171,6 +172,7 @@ TEST_PROGS := \ fold \ hashsum \ head \ + ln \ mkdir \ mv \ nl \ diff --git a/src/ln/ln.rs b/src/ln/ln.rs new file mode 100644 index 000000000..317d0090c --- /dev/null +++ b/src/ln/ln.rs @@ -0,0 +1,329 @@ +#![crate_name = "ln"] +#![feature(path_ext, slice_patterns, str_char)] + +/* + * This file is part of the uutils coreutils package. + * + * (c) Joseph Crail + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +extern crate getopts; + +use std::fs::{self, PathExt}; +use std::io::{BufRead, BufReader, Result, stdin, Write}; +#[cfg(unix)] use std::os::unix::fs::symlink as symlink_file; +#[cfg(windows)] use std::os::windows::fs::symlink_file; +use std::path::{Path, PathBuf}; + +#[path="../common/util.rs"] +#[macro_use] +mod util; + +static NAME: &'static str = "ln"; +static VERSION: &'static str = "1.0.0"; + +pub struct Settings { + overwrite: OverwriteMode, + backup: BackupMode, + suffix: String, + symbolic: bool, + target_dir: Option, + no_target_dir: bool, + verbose: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum OverwriteMode { + NoClobber, + Interactive, + Force, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum BackupMode { + NoBackup, + SimpleBackup, + NumberedBackup, + ExistingBackup, +} + +pub fn uumain(args: Vec) -> i32 { + let mut opts = getopts::Options::new(); + + opts.optflag("b", "", "make a backup of each file that would otherwise be overwritten or removed"); + opts.optflagopt("", "backup", "make a backup of each file that would otherwise be overwritten or removed", "METHOD"); + // TODO: opts.optflag("d", "directory", "allow users with appropriate privileges to attempt to make hard links to directories"); + opts.optflag("f", "force", "remove existing destination files"); + opts.optflag("i", "interactive", "prompt whether to remove existing destination files"); + // TODO: opts.optflag("L", "logical", "dereference TARGETs that are symbolic links"); + // TODO: opts.optflag("n", "no-dereference", "treat LINK_NAME as a normal file if it is a symbolic link to a directory"); + // TODO: opts.optflag("P", "physical", "make hard links directly to symbolic links"); + // TODO: opts.optflag("r", "relative", "create symbolic links relative to link location"); + opts.optflag("s", "symbolic", "make symbolic links instead of hard links"); + opts.optopt("S", "suffix", "override the usual backup suffix", "SUFFIX"); + opts.optopt("t", "target-directory", "specify the DIRECTORY in which to create the links", "DIRECTORY"); + opts.optflag("T", "no-target-directory", "treat LINK_NAME as a normal file always"); + opts.optflag("v", "verbose", "print name of each linked file"); + opts.optflag("h", "help", "display this help and exit"); + opts.optflag("V", "version", "output version information and exit"); + + let matches = match opts.parse(&args[1..]) { + Ok(m) => m, + Err(e) => crash!(1, "{}", e), + }; + + let overwrite_mode = if matches.opt_present("force") { + OverwriteMode::Force + } else if matches.opt_present("interactive") { + OverwriteMode::Interactive + } else { + OverwriteMode::NoClobber + }; + + let backup_mode = if matches.opt_present("b") { + BackupMode::ExistingBackup + } else if matches.opt_present("backup") { + match matches.opt_str("backup") { + None => BackupMode::ExistingBackup, + Some(mode) => match &mode[..] { + "simple" | "never" => BackupMode::SimpleBackup, + "numbered" | "t" => BackupMode::NumberedBackup, + "existing" | "nil" => BackupMode::ExistingBackup, + "none" | "off" => BackupMode::NoBackup, + x => { + show_error!("invalid argument '{}' for 'backup method'\n\ + Try '{} --help' for more information.", x, NAME); + return 1; + } + } + } + } else { + BackupMode::NoBackup + }; + + 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 '{} --help' for more information.", NAME); + return 1; + } + } + } else { + "~".to_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 settings = Settings { + overwrite: overwrite_mode, + backup: backup_mode, + suffix: backup_suffix, + symbolic: matches.opt_present("s"), + target_dir: matches.opt_str("t"), + no_target_dir: matches.opt_present("T"), + verbose: matches.opt_present("v"), + }; + + let string_to_path = |s: &String| { PathBuf::from(s) }; + let paths: Vec = matches.free.iter().map(string_to_path).collect(); + + if matches.opt_present("version") { + println!("{} {}", NAME, VERSION); + 0 + } else if matches.opt_present("help") { + let msg = format!("{0} {1} + +Usage: {0} [OPTION]... [-T] TARGET LINK_NAME (1st form) + or: {0} [OPTION]... TARGET (2nd form) + or: {0} [OPTION]... TARGET... DIRECTORY (3rd form) + or: {0} [OPTION]... -t DIRECTORY TARGET... (4th form) + +In the 1st form, create a link to TARGET with the name LINK_NAME. +In the 2nd form, create a link to TARGET in the current directory. +In the 3rd and 4th forms, create links to each TARGET in DIRECTORY. +Create hard links by default, symbolic links with --symbolic. +By default, each destination (name of new link) should not already exist. +When creating hard links, each TARGET must exist. Symbolic links +can hold arbitrary text; if later resolved, a relative link is +interpreted in relation to its parent directory.", NAME, VERSION); + + print!("{}", opts.usage(&msg)); + 0 + } else { + exec(&paths[..], &settings) + } +} + +fn exec(files: &[PathBuf], settings: &Settings) -> i32 { + match settings.target_dir { + Some(ref name) => return link_files_in_dir(files, &PathBuf::from(name), &settings), + None => {} + } + match files { + [] => { + show_error!("missing file operand\nTry '{} --help' for more information.", NAME); + 1 + }, + [ref target] => match link(target, target, settings) { + Ok(_) => 0, + Err(e) => { + show_error!("{}", e); + 1 + } + }, + [ref target, ref linkname] => match link(target, linkname, settings) { + Ok(_) => 0, + Err(e) => { + show_error!("{}", e); + 1 + } + }, + fs => { + if settings.no_target_dir { + show_error!("extra operand '{}'\nTry '{} --help' for more information.", fs[2].display(), NAME); + return 1; + } + let (targets, dir) = match settings.target_dir { + Some(ref dir) => (fs, PathBuf::from(dir.clone())), + None => (&fs[0..fs.len()-1], fs[fs.len()-1].clone()) + }; + link_files_in_dir(targets, &dir, settings) + } + } +} + +fn link_files_in_dir(files: &[PathBuf], target_dir: &PathBuf, settings: &Settings) -> i32 { + if !target_dir.is_dir() { + show_error!("target '{}' is not a directory", target_dir.display()); + return 1; + } + + let mut all_successful = true; + for srcpath in files.iter() { + let targetpath = match srcpath.as_os_str().to_str() { + Some(name) => target_dir.join(name), + None => { + show_error!("cannot stat '{}': No such file or directory", + srcpath.display()); + all_successful = false; + continue; + } + }; + + match link(srcpath, &targetpath, settings) { + Err(e) => { + show_error!("cannot link '{}' to '{}': {}", + targetpath.display(), srcpath.display(), e); + all_successful = false; + }, + _ => {} + } + } + if all_successful { 0 } else { 1 } +} + +fn link(src: &PathBuf, dst: &PathBuf, settings: &Settings) -> Result<()> { + let mut backup_path = None; + + if dst.is_dir() { + if settings.no_target_dir { + try!(fs::remove_dir(dst)); + } + } + + if is_symlink(dst) || dst.exists() { + match settings.overwrite { + OverwriteMode::NoClobber => {}, + OverwriteMode::Interactive => { + print!("{}: overwrite '{}'? ", NAME, dst.display()); + if !read_yes() { + return Ok(()); + } + try!(fs::remove_file(dst)) + }, + OverwriteMode::Force => { + try!(fs::remove_file(dst)) + } + }; + + backup_path = match settings.backup { + BackupMode::NoBackup => None, + BackupMode::SimpleBackup => Some(simple_backup_path(dst, &settings.suffix)), + BackupMode::NumberedBackup => Some(numbered_backup_path(dst)), + BackupMode::ExistingBackup => Some(existing_backup_path(dst, &settings.suffix)) + }; + if let Some(ref p) = backup_path { + try!(fs::rename(dst, p)); + } + } + + if settings.symbolic { + try!(symlink(src, dst)); + } else { + try!(fs::hard_link(src, dst)); + } + + if settings.verbose { + print!("'{}' -> '{}'", dst.display(), src.display()); + match backup_path { + Some(path) => println!(" (backup: '{}')", path.display()), + None => println!("") + } + } + Ok(()) +} + +fn read_yes() -> bool { + let mut s = String::new(); + match BufReader::new(stdin()).read_line(&mut s) { + Ok(_) => match s.slice_shift_char() { + Some((x, _)) => x == 'y' || x == 'Y', + _ => false + }, + _ => false + } +} + +fn simple_backup_path(path: &PathBuf, suffix: &String) -> PathBuf { + let mut p = path.as_os_str().to_str().unwrap().to_string(); + p.push_str(suffix); + PathBuf::from(p) +} + +fn numbered_backup_path(path: &PathBuf) -> PathBuf { + let mut i: u64 = 1; + loop { + let new_path = simple_backup_path(path, &format!(".~{}~", i)); + if !new_path.exists() { + return new_path; + } + i += 1; + } +} + +fn existing_backup_path(path: &PathBuf, suffix: &String) -> PathBuf { + let test_path = simple_backup_path(path, &".~1~".to_string()); + if test_path.exists() { + return numbered_backup_path(path); + } + simple_backup_path(path, suffix) +} + +pub fn symlink>(src: P, dst: P) -> Result<()> { + symlink_file(src, dst) +} + +pub fn is_symlink>(path: P) -> bool { + match fs::symlink_metadata(path) { + Ok(m) => m.file_type().is_symlink(), + Err(_) => false + } +} diff --git a/test/common/util.rs b/test/common/util.rs index f8db8a8a8..690e7a20a 100644 --- a/test/common/util.rs +++ b/test/common/util.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use std::env; -use std::fs::{self, File}; +use std::fs::{self, File, PathExt}; use std::io::{Read, Write}; #[cfg(unix)] use std::os::unix::fs::symlink as symlink_file; @@ -83,6 +83,34 @@ pub fn symlink(src: &str, dst: &str) { symlink_file(src, dst).unwrap(); } +pub fn is_symlink(path: &str) -> bool { + match fs::symlink_metadata(path) { + Ok(m) => m.file_type().is_symlink(), + Err(_) => false + } +} + +pub fn resolve_link(path: &str) -> String { + match fs::read_link(path) { + Ok(p) => p.to_str().unwrap().to_owned(), + Err(_) => "".to_string() + } +} + +pub fn file_exists(path: &str) -> bool { + match fs::metadata(path) { + Ok(m) => m.is_file(), + Err(_) => false + } +} + +pub fn dir_exists(path: &str) -> bool { + match fs::metadata(path) { + Ok(m) => m.is_dir(), + Err(_) => false + } +} + pub fn cleanup(path: &'static str) { let p = Path::new(path); match fs::metadata(p) { diff --git a/test/ln.rs b/test/ln.rs new file mode 100644 index 000000000..9020dc0c5 --- /dev/null +++ b/test/ln.rs @@ -0,0 +1,354 @@ +extern crate libc; + +use std::process::Command; +use util::*; + +static PROGNAME: &'static str = "./ln"; + +#[path = "common/util.rs"] +#[macro_use] +mod util; + +#[test] +fn test_symlink_existing_file() { + let file = "test_symlink_existing_file"; + let link = "test_symlink_existing_file_link"; + + touch(file); + + let result = run(Command::new(PROGNAME).args(&["-s", file, link])); + assert_empty_stderr!(result); + assert!(result.success); + assert!(file_exists(file)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); +} + +#[test] +fn test_symlink_dangling_file() { + let file = "test_symlink_dangling_file"; + let link = "test_symlink_dangling_file_link"; + + let result = run(Command::new(PROGNAME).args(&["-s", file, link])); + assert_empty_stderr!(result); + assert!(result.success); + assert!(!file_exists(file)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); +} + +#[test] +fn test_symlink_existing_directory() { + let dir = "test_symlink_existing_dir"; + let link = "test_symlink_existing_dir_link"; + + mkdir(dir); + + let result = run(Command::new(PROGNAME).args(&["-s", dir, link])); + assert_empty_stderr!(result); + assert!(result.success); + assert!(dir_exists(dir)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), dir); +} + +#[test] +fn test_symlink_dangling_directory() { + let dir = "test_symlink_dangling_dir"; + let link = "test_symlink_dangling_dir_link"; + + let result = run(Command::new(PROGNAME).args(&["-s", dir, link])); + assert_empty_stderr!(result); + assert!(result.success); + assert!(!dir_exists(dir)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), dir); +} + +#[test] +fn test_symlink_circular() { + let link = "test_symlink_circular"; + + let result = run(Command::new(PROGNAME).args(&["-s", link])); + assert_empty_stderr!(result); + assert!(result.success); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), link); +} + +#[test] +fn test_symlink_dont_overwrite() { + let file = "test_symlink_dont_overwrite"; + let link = "test_symlink_dont_overwrite_link"; + + touch(file); + touch(link); + + let result = run(Command::new(PROGNAME).args(&["-s", file, link])); + assert!(!result.success); + assert!(file_exists(file)); + assert!(file_exists(link)); + assert!(!is_symlink(link)); +} + +#[test] +fn test_symlink_overwrite_force() { + let file_a = "test_symlink_overwrite_force_a"; + let file_b = "test_symlink_overwrite_force_b"; + let link = "test_symlink_overwrite_force_link"; + + // Create symlink + symlink(file_a, link); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file_a); + + // Force overwrite of existing symlink + let result = run(Command::new(PROGNAME).args(&["--force", "-s", file_b, link])); + assert!(result.success); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file_b); +} + +#[test] +fn test_symlink_interactive() { + let file = "test_symlink_interactive_file"; + let link = "test_symlink_interactive_file_link"; + + touch(file); + touch(link); + + let result1 = run_piped_stdin(Command::new(PROGNAME).args(&["-i", "-s", file, link]), b"n"); + + assert_empty_stderr!(result1); + assert!(result1.success); + + assert!(file_exists(file)); + assert!(!is_symlink(link)); + + let result2 = run_piped_stdin(Command::new(PROGNAME).args(&["-i", "-s", file, link]), b"Yesh"); + + assert_empty_stderr!(result2); + assert!(result2.success); + + assert!(file_exists(file)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); +} + +#[test] +fn test_symlink_simple_backup() { + let file = "test_symlink_simple_backup"; + let link = "test_symlink_simple_backup_link"; + + touch(file); + symlink(file, link); + assert!(file_exists(file)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); + + let result = run(Command::new(PROGNAME).args(&["-b", "-s", file, link])); + + assert_empty_stderr!(result); + assert!(result.success); + assert!(file_exists(file)); + + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); + + let backup = &format!("{}~", link); + assert!(is_symlink(backup)); + assert_eq!(resolve_link(backup), file); +} + +#[test] +fn test_symlink_custom_backup_suffix() { + let file = "test_symlink_custom_backup_suffix"; + let link = "test_symlink_custom_backup_suffix_link"; + let suffix = "super-suffix-of-the-century"; + + touch(file); + symlink(file, link); + assert!(file_exists(file)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); + + let arg = &format!("--suffix={}", suffix); + let result = run(Command::new(PROGNAME).args(&["-b", arg, "-s", file, link])); + + assert_empty_stderr!(result); + assert!(result.success); + assert!(file_exists(file)); + + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); + + let backup = &format!("{}{}", link, suffix); + assert!(is_symlink(backup)); + assert_eq!(resolve_link(backup), file); +} + +#[test] +fn test_symlink_backup_numbering() { + let file = "test_symlink_backup_numbering"; + let link = "test_symlink_backup_numbering_link"; + + touch(file); + symlink(file, link); + assert!(file_exists(file)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); + + let result = run(Command::new(PROGNAME).args(&["-s", "--backup=t", file, link])); + + assert_empty_stderr!(result); + assert!(result.success); + assert!(file_exists(file)); + + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); + + let backup = &format!("{}.~1~", link); + assert!(is_symlink(backup)); + assert_eq!(resolve_link(backup), file); +} + +#[test] +fn test_symlink_existing_backup() { + let file = "test_symlink_existing_backup"; + let link = "test_symlink_existing_backup_link"; + let link_backup = "test_symlink_existing_backup_link.~1~"; + let resulting_backup = "test_symlink_existing_backup_link.~2~"; + + // Create symlink and verify + touch(file); + symlink(file, link); + assert!(file_exists(file)); + assert!(is_symlink(link)); + assert_eq!(resolve_link(link), file); + + // Create backup symlink and verify + symlink(file, link_backup); + assert!(file_exists(file)); + assert!(is_symlink(link_backup)); + assert_eq!(resolve_link(link_backup), file); + + let result = run(Command::new(PROGNAME).args(&["-s", "--backup=nil", file, link])); + + assert_empty_stderr!(result); + assert!(result.success); + assert!(file_exists(file)); + + assert!(is_symlink(link_backup)); + assert_eq!(resolve_link(link_backup), file); + + assert!(is_symlink(resulting_backup)); + assert_eq!(resolve_link(resulting_backup), file); +} + +#[test] +fn test_symlink_target_dir() { + let dir = "test_ln_target_dir_dir"; + let file_a = "test_ln_target_dir_file_a"; + let file_b = "test_ln_target_dir_file_b"; + + touch(file_a); + touch(file_b); + mkdir(dir); + + let result = run(Command::new(PROGNAME).args(&["-s", "-t", dir, file_a, file_b])); + + assert_empty_stderr!(result); + assert!(result.success); + + let file_a_link = &format!("{}/{}", dir, file_a); + assert!(is_symlink(file_a_link)); + assert_eq!(resolve_link(file_a_link), file_a); + + let file_b_link = &format!("{}/{}", dir, file_b); + assert!(is_symlink(file_b_link)); + assert_eq!(resolve_link(file_b_link), file_b); +} + +#[test] +fn test_symlink_overwrite_dir() { + let path_a = "test_symlink_overwrite_dir_a"; + let path_b = "test_symlink_overwrite_dir_b"; + + touch(path_a); + mkdir(path_b); + + let result = run(Command::new(PROGNAME).args(&["-s", "-T", path_a, path_b])); + + assert_empty_stderr!(result); + assert!(result.success); + + assert!(file_exists(path_a)); + assert!(is_symlink(path_b)); + assert_eq!(resolve_link(path_b), path_a); +} + +#[test] +fn test_symlink_overwrite_nonempty_dir() { + let path_a = "test_symlink_overwrite_nonempty_dir_a"; + let path_b = "test_symlink_overwrite_nonempty_dir_b"; + let dummy = "test_symlink_overwrite_nonempty_dir_b/file"; + + touch(path_a); + mkdir(path_b); + touch(dummy); + + let result = run(Command::new(PROGNAME).args(&["-v", "-T", "-s", path_a, path_b])); + + // Not same error as GNU; the error message is a Rust builtin + // TODO: test (and implement) correct error message (or at least decide whether to do so) + // Current: "ln: error: Directory not empty (os error 66)" + // GNU: "ln: cannot link 'a' to 'b': Directory not empty" + assert!(result.stderr.len() > 0); + + // Verbose output for the link should not be shown on failure + assert!(result.stdout.len() == 0); + + assert!(!result.success); + assert!(file_exists(path_a)); + assert!(dir_exists(path_b)); +} + +#[test] +fn test_symlink_errors() { + let dir = "test_symlink_errors_dir"; + let file_a = "test_symlink_errors_file_a"; + let file_b = "test_symlink_errors_file_b"; + + mkdir(dir); + touch(file_a); + touch(file_b); + + // $ ln -T -t a b + // ln: cannot combine --target-directory (-t) and --no-target-directory (-T) + let result = run(Command::new(PROGNAME).args(&["-T", "-t", dir, file_a, file_b])); + assert_eq!(result.stderr, + "ln: error: cannot combine --target-directory (-t) and --no-target-directory (-T)\n"); + assert!(!result.success); +} + +#[test] +fn test_symlink_verbose() { + let file_a = "test_symlink_verbose_file_a"; + let file_b = "test_symlink_verbose_file_b"; + + touch(file_a); + + let result = run(Command::new(PROGNAME).args(&["-v", file_a, file_b])); + assert_empty_stderr!(result); + assert_eq!(result.stdout, + format!("'{}' -> '{}'\n", file_b, file_a)); + assert!(result.success); + + touch(file_b); + + let result = run(Command::new(PROGNAME).args(&["-v", "-b", file_a, file_b])); + assert_empty_stderr!(result); + assert_eq!(result.stdout, + format!("'{}' -> '{}' (backup: '{}~')\n", file_b, file_a, file_b)); + assert!(result.success); +}