mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-29 12:07:46 +00:00
Add initial implementation for ln.
There are several areas needing improvement: 1) add tests for hard links 2) add implementation for uncommon flags (-d, -L, -n, -P, -r) 3) align error messages more closely with GNU implementation
This commit is contained in:
parent
942fe0d7e0
commit
9f1dc98925
4 changed files with 714 additions and 1 deletions
2
Makefile
2
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 \
|
||||
|
|
329
src/ln/ln.rs
Normal file
329
src/ln/ln.rs
Normal file
|
@ -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 <jbcrail@gmail.com>
|
||||
*
|
||||
* 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<String>,
|
||||
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<String>) -> 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<PathBuf> = 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<P: AsRef<Path>>(src: P, dst: P) -> Result<()> {
|
||||
symlink_file(src, dst)
|
||||
}
|
||||
|
||||
pub fn is_symlink<P: AsRef<Path>>(path: P) -> bool {
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(m) => m.file_type().is_symlink(),
|
||||
Err(_) => false
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
354
test/ln.rs
Normal file
354
test/ln.rs
Normal file
|
@ -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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue